Skip to main content

bloq_ir/ir/
node.rs

1use std::fmt;
2use std::sync::Arc;
3
4use bloq_circuit::{Basis, PauliMap};
5use glam::IVec3;
6
7use super::{
8    DetectorBundleUse, InstanceMeasurement, NodeDetector, NodeRestart, SourceBlockRef, SubGraph,
9    TemplateInstance, TemplateInstanceId, TemporalPipeRef,
10};
11
12/// What source-level construct an IR node realizes.
13#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
14pub enum NodeProvenance {
15    /// A quantum node realizing one connected component of source blocks (its
16    /// `members`), fused because they share physical qubits on a layer.
17    BlockComponent {
18        /// Source blocks in the component.
19        members: Vec<SourceBlockRef>,
20    },
21    /// A quantum node realizing one temporal pipe: the seam layer between two
22    /// block layers. Normally a temporal pipe will not compile to real quantum
23    /// circuits, the currently only use case if for temporal hadamard pipe
24    /// which includes a realignment layer.
25    TemporalPipe {
26        /// Realized temporal pipe.
27        pipe: TemporalPipeRef,
28    },
29    /// Compiler-only replacement of a source spatial Port. The virtual
30    /// temporal-Port node has no lattice cell of its own; `role` places it just
31    /// before or after `source` on the doubled-z schedule.
32    SpatialPortSubstitution {
33        /// Source port position.
34        source: IVec3,
35        /// Source port role.
36        role: bloq_utils::PortRole,
37    },
38    /// Memory-round padding inserted at a `Quantum` seam or region terminal
39    /// post-compile ([`crate::Bloq::insert_memory_rounds`]). Like
40    /// [`Self::TemporalPipe`] it lives on the pipe's layer, but it carries the
41    /// wait duration so consumers can find padding nodes and read
42    /// their round count without inspecting the template's repeat structure.
43    MemoryPadding {
44        /// Padded pipe.
45        pipe: TemporalPipeRef,
46        /// Inserted memory rounds.
47        rounds: u32,
48    },
49    /// A classical node realizing stabilizer generator `ordinal`'s readout
50    /// recipe: its complete `Observable`, any shared fragments, and the
51    /// corrected-value `Compute`s derived from them.
52    Generator {
53        /// Source generator ordinal.
54        ordinal: u32,
55    },
56    /// A classical node lowered from the source action at `ordinal` (program
57    /// action order): a named readout's complete `Observable`, a
58    /// condition/selector `Compute`, or a `Discard` sink.
59    Action {
60        /// Source action ordinal.
61        ordinal: u32,
62    },
63    /// A source branch's resolved selector, shared by every assembly stage.
64    BranchSelector {
65        /// Source selector name.
66        name: String,
67    },
68    /// A frame-sign `Compute`: the `basis`-frame correction bit for source
69    /// output port `port`. The stamp **is** the frame
70    /// table: [`crate::Bloq::output_frames`] derives its [`crate::FramePair`]s from
71    /// these stamps, so there is no side table to drift. Validation enforces
72    /// that a stamp sits on a top-level `Compute` and that each output carries
73    /// exactly one X and one Z stamp.
74    // Spec rule WF-15.
75    OutputFrame {
76        /// Source output port.
77        port: IVec3,
78        /// Corrected Pauli basis.
79        basis: Basis,
80    },
81    /// No provenance: hand-built or synthetic (e.g. a constant producer).
82    None,
83}
84
85/// Human-readable one-line description, e.g. `blocks (0,0,0) (1,0,0)` or
86/// `pipe (0,0,0)>(0,0,1)`. Coordinate/pipe spelling matches the `.bloq` text
87/// format so the two read alike; consumers are the program view and the Stim
88/// backend's per-node provenance comments.
89impl fmt::Display for NodeProvenance {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        let coord3 = |f: &mut fmt::Formatter<'_>, p: IVec3| write!(f, "({},{},{})", p.x, p.y, p.z);
92        match self {
93            NodeProvenance::None => write!(f, "synthetic"),
94            NodeProvenance::BlockComponent { members } => {
95                write!(f, "blocks")?;
96                for member in members {
97                    write!(f, " ")?;
98                    coord3(f, member.pos)?;
99                }
100                Ok(())
101            }
102            NodeProvenance::TemporalPipe { pipe } => {
103                write!(f, "pipe ")?;
104                coord3(f, pipe.src)?;
105                write!(f, ">")?;
106                coord3(f, pipe.dst)?;
107                if pipe.hadamard {
108                    write!(f, " H")?;
109                }
110                Ok(())
111            }
112            NodeProvenance::SpatialPortSubstitution { source, role } => {
113                write!(f, "spatial-port {} ", role.as_str())?;
114                coord3(f, *source)
115            }
116            NodeProvenance::MemoryPadding { pipe, rounds } => {
117                write!(f, "padding ")?;
118                coord3(f, pipe.src)?;
119                write!(f, ">")?;
120                coord3(f, pipe.dst)?;
121                write!(f, " rounds {rounds}")
122            }
123            NodeProvenance::Generator { ordinal } => write!(f, "generator {ordinal}"),
124            NodeProvenance::Action { ordinal } => write!(f, "action {ordinal}"),
125            NodeProvenance::BranchSelector { name } => write!(f, "selector {name}"),
126            NodeProvenance::OutputFrame { port, basis } => {
127                let basis = match basis {
128                    Basis::X => "x",
129                    Basis::Z => "z",
130                };
131                write!(f, "frame {basis} ")?;
132                coord3(f, *port)
133            }
134        }
135    }
136}
137
138/// A graph node, tagged by what kind of work it represents.
139///
140/// `kind` is what the node computes; `provenance` is why it exists — its tie
141/// back to the source block graph, a stabilizer generator, a source action, or
142/// an output frame ([`NodeProvenance`]).
143#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
144pub struct BloqNode {
145    /// Node payload.
146    pub kind: BloqNodeKind,
147    /// Source provenance.
148    pub provenance: NodeProvenance,
149    /// Optional activation `Value` slot for a classical or region node. When false,
150    /// bit results are zero, boundary bindings are empty, and no read, decoder
151    /// request, region-body execution, or discard occurs. It is not a data operand.
152    pub activation: Option<u32>,
153}
154
155/// What a [`BloqNode`] computes.
156#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
157pub enum BloqNodeKind {
158    /// Shared until quantum data is edited. Large physical payloads do not
159    /// inflate classical node slots or copy during unrelated graph edits.
160    Quantum(#[serde(deserialize_with = "deserialize_quantum")] Arc<QuantumNode>),
161    /// Shared immutable classical definition. Activation, inputs and provenance
162    /// belong to this node's invocation; editing detaches only its definition.
163    Classical(Arc<ClassicalNode>),
164    /// Structured control-flow region.
165    Region(RegionNode),
166}
167
168impl BloqNodeKind {
169    /// Borrow a classical definition without changing its sharing.
170    pub fn try_classical(&self) -> Option<&ClassicalNode> {
171        match self {
172            Self::Classical(data) => Some(data),
173            Self::Quantum(_) | Self::Region(_) => None,
174        }
175    }
176
177    /// Detach a shared classical definition before editing it.
178    pub fn try_classical_mut(&mut self) -> Option<&mut ClassicalNode> {
179        match self {
180            Self::Classical(data) => Some(Arc::make_mut(data)),
181            Self::Quantum(_) | Self::Region(_) => None,
182        }
183    }
184}
185
186pub(super) fn deserialize_quantum<'de, D: serde::Deserializer<'de>>(
187    deserializer: D,
188) -> Result<Arc<QuantumNode>, D::Error> {
189    // Serde's generic Arc decoder goes through Box<T>. Allocate the final
190    // shared payload directly instead of boxing and reallocating each node.
191    <QuantumNode as serde::Deserialize<'de>>::deserialize(deserializer).map(Arc::new)
192}
193
194/// Classical logic and observable recipes. Composition edges include a child's
195/// measurements and boundary bindings; value edges select a Boolean output.
196#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
197pub enum ClassicalNode {
198    /// Combinational logic over incoming value slots.
199    Compute {
200        /// Boolean expression.
201        expr: ClassicalExpr,
202    },
203    /// Observable recipe. Indexed recipes expose corrected values and
204    /// predicted decoder flips. Unindexed fragments expose their assembled parity and
205    /// compose without an independent decoder request.
206    Observable {
207        /// Graph-local decoder query identity; absent for recipe fragments.
208        index: Option<u32>,
209        /// Local records, preserving order and occurrences.
210        #[serde(with = "crate::binary::instance_measurements")]
211        measurements: Vec<InstanceMeasurement>,
212        /// Boundary bindings with physical ownership.
213        operators: Vec<InstanceBoundaryOperator>,
214    },
215    /// Reject the shot when the condition holds.
216    Discard {
217        /// Rejection condition.
218        condition: ClassicalExpr,
219    },
220}
221
222impl ClassicalNode {
223    /// A complete observable with an empty recipe.
224    pub fn observable(index: u32) -> Self {
225        Self::Observable {
226            index: Some(index),
227            measurements: Vec::new(),
228            operators: Vec::new(),
229        }
230    }
231
232    /// A composable recipe without its own decoder query.
233    pub fn observable_fragment(
234        measurements: Vec<InstanceMeasurement>,
235        operators: Vec<InstanceBoundaryOperator>,
236    ) -> Self {
237        Self::Observable {
238            index: None,
239            measurements,
240            operators,
241        }
242    }
243
244    /// An empty composable recipe.
245    pub fn fragment() -> Self {
246        Self::observable_fragment(Vec::new(), Vec::new())
247    }
248
249    /// Record terms owned directly by this producer.
250    pub fn measurements(&self) -> &[InstanceMeasurement] {
251        match self {
252            Self::Observable { measurements, .. } => measurements,
253            _ => &[],
254        }
255    }
256
257    /// Boundary bindings owned directly by this producer.
258    pub fn operators(&self) -> &[InstanceBoundaryOperator] {
259        match self {
260            Self::Observable { operators, .. } => operators,
261            _ => &[],
262        }
263    }
264}
265
266/// Combinational logic over a classical node's incoming `Value` edges.
267///
268/// [`ClassicalExpr::In`] reads the edge whose `slot` equals its index
269/// (SSA-by-slot): the slot is carried on the edge, so the binding survives edge
270/// reordering under edits rather than depending on iteration order.
271#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
272pub enum ClassicalExpr {
273    /// Read one incoming edge slot.
274    In(u32),
275    /// A compile-time-constant bit — an input-less producer. Carries a
276    /// deterministic feedback sign (constant-sign folds to no op).
277    Const(bool),
278    /// Boolean negation.
279    Not(Box<ClassicalExpr>),
280    /// Ordered parity operands; the empty parity is false.
281    Xor(Box<[ClassicalExpr]>),
282    /// Ordered conjunction operands; the empty conjunction is true.
283    And(Box<[ClassicalExpr]>),
284    /// Ordered disjunction operands; the empty disjunction is false.
285    Or(Box<[ClassicalExpr]>),
286    /// `[condition, when_false, when_true]`. All three operands must be known,
287    /// including the unselected value; this is combinational dataflow (SEM-EXPR).
288    Select(Box<[ClassicalExpr; 3]>),
289    /// Affine parity of ordered input slots and a constant XOR term.
290    /// Repeated slots stay present: every input must be known even when its
291    /// Boolean contributions cancel (SEM-EXPR). No expression nodes per slot.
292    Parity {
293        /// Ordered input slots.
294        inputs: Box<[u32]>,
295        /// Constant XOR term.
296        constant: bool,
297    },
298}
299
300impl ClassicalExpr {
301    /// Build an affine parity, retaining input order and multiplicity.
302    #[must_use]
303    pub fn parity(inputs: impl IntoIterator<Item = u32>, constant: bool) -> Self {
304        let inputs: Box<[_]> = inputs.into_iter().collect();
305        match inputs.as_ref() {
306            [] => Self::Const(constant),
307            [slot] if !constant => Self::In(*slot),
308            _ => Self::Parity { inputs, constant },
309        }
310    }
311
312    /// Build a parity list, collapsing empty and singleton lists.
313    /// Preserves operand order, duplicates, and nested expressions.
314    ///
315    /// # Panics
316    ///
317    /// Panics only if the collected operand count changes before extraction.
318    #[must_use]
319    pub fn xor(operands: impl IntoIterator<Item = Self>) -> Self {
320        let mut operands: Vec<_> = operands.into_iter().collect();
321        match operands.len() {
322            0 => Self::Const(false),
323            1 => operands
324                .pop()
325                .expect("the single operand was counted above"),
326            _ => Self::Xor(operands.into_boxed_slice()),
327        }
328    }
329
330    /// Select a value with explicit false/true operand order (SEM-EXPR).
331    /// Literal branches use smaller operators without dropping unknown inputs.
332    #[must_use]
333    pub fn select(condition: Self, when_false: Self, when_true: Self) -> Self {
334        match (when_false, when_true) {
335            (Self::Const(false), Self::Const(true)) => condition,
336            (Self::Const(true), Self::Const(false)) => Self::Not(Box::new(condition)),
337            (Self::Const(false), when_true) => Self::And(Box::new([condition, when_true])),
338            (when_false, Self::Const(true)) => Self::Or(Box::new([condition, when_false])),
339            // A negated gate needs two boxes; its selection needs only one.
340            (when_false, when_true) => Self::Select(Box::new([condition, when_false, when_true])),
341        }
342    }
343
344    /// Direct expression children in evaluation order. Compact [`Self::Parity`]
345    /// stores slots directly; use [`Self::for_each_input`] to visit all inputs.
346    pub fn operands(&self) -> &[Self] {
347        match self {
348            Self::In(_) | Self::Const(_) | Self::Parity { .. } => &[],
349            Self::Not(inner) => std::slice::from_ref(inner),
350            Self::Xor(operands) | Self::And(operands) | Self::Or(operands) => operands,
351            Self::Select(operands) => operands.as_ref(),
352        }
353    }
354
355    /// Fold the expression over three-valued inputs: `None` (an input that is
356    /// unevaluable in the caller's context) propagates. The reference
357    /// evaluator every consumer shares.
358    pub fn eval(&self, input: &mut impl FnMut(u32) -> Option<bool>) -> Option<bool> {
359        match self {
360            Self::In(slot) => input(*slot),
361            Self::Const(bit) => Some(*bit),
362            Self::Parity { inputs, constant } => inputs
363                .iter()
364                .try_fold(*constant, |value, &slot| Some(value ^ input(slot)?)),
365            Self::Not(inner) => inner.eval(input).map(|bit| !bit),
366            Self::Xor(operands) => operands
367                .iter()
368                .try_fold(false, |value, expr| Some(value ^ expr.eval(input)?)),
369            Self::And(operands) => operands
370                .iter()
371                .try_fold(true, |value, expr| Some(value & expr.eval(input)?)),
372            Self::Or(operands) => operands
373                .iter()
374                .try_fold(false, |value, expr| Some(value | expr.eval(input)?)),
375            Self::Select(operands) => {
376                let [condition, when_false, when_true] = operands.as_ref();
377                let condition = condition.eval(input)?;
378                let when_false = when_false.eval(input)?;
379                let when_true = when_true.eval(input)?;
380                Some(if condition { when_true } else { when_false })
381            }
382        }
383    }
384
385    /// Visit every `In(slot)` leaf, left to right.
386    pub fn for_each_input(&self, f: &mut impl FnMut(u32)) {
387        match self {
388            Self::In(slot) => f(*slot),
389            Self::Parity { inputs, .. } => inputs.iter().for_each(|&slot| f(slot)),
390            _ => {}
391        }
392        for operand in self.operands() {
393            operand.for_each_input(f);
394        }
395    }
396
397    /// Replace original leaves in order, moving replacement subtrees into
398    /// place: slot remapping (`f` returns another `In`) and producer inlining
399    /// (`f` returns a subtree) in one operation.
400    ///
401    /// Newly inserted leaves are not visited again.
402    pub(crate) fn replace_inputs(&mut self, f: &mut impl FnMut(u32) -> ClassicalExpr) {
403        match self {
404            Self::In(slot) => *self = f(*slot),
405            Self::Const(_) => {}
406            Self::Parity { inputs, constant } => {
407                let mut operands = Vec::with_capacity(inputs.len());
408                for &slot in inputs.iter() {
409                    operands.push(f(slot));
410                }
411                if operands
412                    .iter()
413                    .all(|operand| matches!(operand, Self::In(_)))
414                {
415                    for (slot, operand) in inputs.iter_mut().zip(operands) {
416                        let Self::In(mapped) = operand else {
417                            unreachable!("all replacement operands were checked as inputs")
418                        };
419                        *slot = mapped;
420                    }
421                } else {
422                    let expr = Self::xor(operands);
423                    *self = if *constant {
424                        Self::Not(Box::new(expr))
425                    } else {
426                        expr
427                    };
428                }
429            }
430            Self::Not(inner) => inner.replace_inputs(f),
431            Self::Xor(operands) | Self::And(operands) | Self::Or(operands) => {
432                for operand in operands {
433                    operand.replace_inputs(f);
434                }
435            }
436            Self::Select(operands) => {
437                for operand in operands.as_mut() {
438                    operand.replace_inputs(f);
439                }
440            }
441        }
442    }
443
444    /// Whether the expression is XOR-expressible (`In`/`Const`/`Not`/`Xor`/`Parity`
445    /// only). A static backend folds a linear condition into
446    /// `OBSERVABLE_INCLUDE`; a nonlinear (`And`/`Or`/`Select`) one needs a
447    /// dynamic backend.
448    pub fn is_linear(&self) -> bool {
449        match self {
450            Self::In(_) | Self::Const(_) | Self::Parity { .. } => true,
451            Self::Not(inner) => inner.is_linear(),
452            Self::Xor(operands) => operands.iter().all(Self::is_linear),
453            Self::And(..) | Self::Or(..) | Self::Select(..) => false,
454        }
455    }
456
457    /// Compact affine subexpressions after graph rewrites. Never cancel input
458    /// slots: Boolean equality alone does not preserve unavailable inputs.
459    pub(crate) fn needs_affine_compaction(&self) -> bool {
460        if matches!(self, Self::Not(_) | Self::Xor(_)) && self.is_linear() {
461            return true;
462        }
463        self.operands().iter().any(Self::needs_affine_compaction)
464    }
465
466    /// Compact affine subexpressions while preserving ordered input occurrences.
467    pub(crate) fn compact_affine(&mut self) {
468        if matches!(self, Self::In(_) | Self::Const(_) | Self::Parity { .. }) {
469            return;
470        }
471        if self.is_linear() {
472            let constant = self
473                .eval(&mut |_| Some(false))
474                .expect("constant assignment");
475            let mut inputs = Vec::new();
476            self.for_each_input(&mut |slot| inputs.push(slot));
477            *self = Self::parity(inputs, constant);
478            return;
479        }
480        match self {
481            Self::Not(inner) => inner.compact_affine(),
482            Self::Xor(operands) | Self::And(operands) | Self::Or(operands) => {
483                for operand in operands {
484                    operand.compact_affine();
485                }
486            }
487            Self::Select(operands) => {
488                for operand in operands.as_mut() {
489                    operand.compact_affine();
490                }
491            }
492            _ => unreachable!("leaves returned above"),
493        }
494    }
495}
496
497/// Which temporal face of an instance a boundary operator binds.
498///
499/// The face fixes emission position: an `Input` (−Z) operator reads the qubit
500/// state entering the instance, so it emits *before* the instance circuit; an
501/// `Output` (+Z) operator reads on exit, so it emits *after*.
502#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
503pub enum BoundaryFace {
504    /// State entering the instance.
505    Input,
506    /// State leaving the instance.
507    Output,
508}
509
510/// A symbolic logical operator at one template-instance boundary.
511///
512/// This is one binding payload element of [`ClassicalNode::Observable`]. It names the
513/// operator by instance reference (never per-shot bits), in instance-global
514/// qubit coordinates — a translated [`PauliMap`] at one temporal `face`.
515#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
516pub struct InstanceBoundaryOperator {
517    /// Owning template instance.
518    pub instance: TemplateInstanceId,
519    /// Bound temporal face.
520    pub face: BoundaryFace,
521    /// Logical Pauli operator.
522    pub operator: PauliMap,
523}
524
525/// A structured control-flow region.
526///
527/// Its interface is the body's declared bit result and boundary bindings. Its predicate arrives on a
528/// `Value` edge; its body is a [`SubGraph`] over the same global template pool.
529#[derive(Debug, Clone)]
530pub enum RegionNode {
531    /// Region carrying a postselection/restart predicate. Execution retries a
532    /// sandboxed body until its restart predicate is false, subject to a
533    /// configured execution cap. Decoder results affect retries only through
534    /// the explicit predicate; physical restart parities can abort an attempt early.
535    ///
536    /// `restart_condition` reads incoming `Value` slots; unfed slots read the
537    /// optional body-local `restart_source`. Constant predicates need no source.
538    /// The body's declared result remains independent of the restart predicate.
539    RepeatUntilSuccess {
540        /// Retried body.
541        body: SubGraph,
542        /// Restart predicate.
543        restart_condition: ClassicalExpr,
544        /// Optional body-local value feeding the predicate's unfed slots.
545        /// Absent when every predicate input is supplied by incoming edges;
546        /// constant predicates need no source.
547        // Spec rule WF-10.
548        restart_source: Option<super::ValueRef>,
549    },
550}
551
552// Keep the existing RUS binary discriminant (1). The removed variant (0)
553// must be rejected rather than interpreted as a retry region.
554#[derive(serde::Serialize, serde::Deserialize)]
555enum RegionWire<B, E> {
556    Removed,
557    RepeatUntilSuccess {
558        body: B,
559        restart_condition: E,
560        restart_source: Option<super::ValueRef>,
561    },
562}
563
564impl serde::Serialize for RegionNode {
565    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
566        let Self::RepeatUntilSuccess {
567            body,
568            restart_condition,
569            restart_source,
570        } = self;
571        RegionWire::RepeatUntilSuccess {
572            body,
573            restart_condition,
574            restart_source: *restart_source,
575        }
576        .serialize(serializer)
577    }
578}
579
580impl<'de> serde::Deserialize<'de> for RegionNode {
581    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
582        match RegionWire::deserialize(deserializer)? {
583            RegionWire::Removed => Err(serde::de::Error::custom("removed region variant")),
584            RegionWire::RepeatUntilSuccess {
585                body,
586                restart_condition,
587                restart_source,
588            } => Ok(Self::RepeatUntilSuccess {
589                body,
590                restart_condition,
591                restart_source,
592            }),
593        }
594    }
595}
596
597/// The body of a [`RegionNode`].
598#[derive(
599    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
600)]
601pub enum BodySelector {
602    /// The region's sole body.
603    Body,
604}
605
606impl RegionNode {
607    /// The region's body subgraph.
608    pub fn bodies(&self) -> impl Iterator<Item = (BodySelector, &SubGraph)> {
609        let Self::RepeatUntilSuccess { body, .. } = self;
610        std::iter::once((BodySelector::Body, body))
611    }
612
613    /// The mutable mirror of [`Self::bodies`].
614    pub fn bodies_mut(&mut self) -> impl Iterator<Item = (BodySelector, &mut SubGraph)> {
615        let Self::RepeatUntilSuccess { body, .. } = self;
616        std::iter::once((BodySelector::Body, body))
617    }
618
619    /// The region variant's display name.
620    pub fn kind_name(&self) -> &'static str {
621        "RepeatUntilSuccess"
622    }
623}
624
625/// Cumulative round cuts projecting a multi-layer block onto source z layers.
626#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
627pub struct QuantumTimeline {
628    /// Cumulative logical-round ends, one entry per occupied source z layer.
629    /// The first entry belongs to the node's lowest source layer, derived from
630    /// [`BloqNode::layer`]. Equal adjacent entries represent a layer containing
631    /// no circuit rounds.
632    pub layer_round_ends: Vec<u32>,
633}
634
635/// A template-backed quantum node: its ops come from instantiating the
636/// referenced templates from the pool, not from any per-node circuit.
637///
638/// Instances must have merge-compatible template entry bodies: aligned
639/// tick/repeat segments, matching repeat counts, and no same-segment qubit
640/// overlap except duplicate measurements or duplicate same-basis resets.
641/// [`Bloq::validate`](crate::Bloq::validate) enforces this for debug
642/// compilation, so instantiation and backends may assume it.
643#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
644pub struct QuantumNode {
645    /// Placed circuit templates.
646    pub instances: Vec<TemplateInstance>,
647    /// Cross-template detectors.
648    pub detectors: Vec<NodeDetector>,
649    /// Shared cross-template detector rows bound to this node's instances.
650    pub detector_bundles: Vec<DetectorBundleUse>,
651    /// Cross-instance restart syndromes; see [`NodeRestart`].
652    pub restarts: Vec<NodeRestart>,
653    /// Source-layer projection for a multi-z block component. Single-layer and
654    /// synthetic nodes need no timeline.
655    pub timeline: Option<QuantumTimeline>,
656    /// Conditional registrations in this component. Unlisted members and side
657    /// tables are common to every selection. All inputs must be ready before
658    /// the component starts; registration itself performs no quantum work.
659    pub guards: Vec<QuantumGuard>,
660}
661
662/// Members and side tables enabled by one quantum node's `Value` input.
663///
664/// Each instance/detector/restart may occur in at most one registration; a
665/// compound condition is an ordinary `Compute` producer feeding `input`.
666#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
667pub struct QuantumGuard {
668    /// Incoming selector slot.
669    pub input: u32,
670    /// Conditionally enabled instances.
671    pub instances: Vec<TemplateInstanceId>,
672    /// Conditionally enabled detector indices.
673    pub detectors: Vec<u32>,
674    /// Conditionally enabled detector bundle use indices.
675    pub detector_bundles: Vec<u32>,
676    /// Conditionally enabled restart indices.
677    pub restarts: Vec<u32>,
678    /// Parity contributions XORed into selected detector rows by this input.
679    pub detector_parities: Vec<(u32, crate::NodeDetectorParity)>,
680    /// Parity contributions XORed into selected restart rows by this input.
681    pub restart_parities: Vec<(u32, crate::NodeDetectorParity)>,
682}
683
684impl QuantumNode {
685    /// Every parity stored on this node: the inline detector rows, then the
686    /// restart rows, then each guard's side-table contributions.
687    ///
688    /// Pooled [`DetectorBundleUse`] rows are deliberately *not* included — they
689    /// live in the program's bundle pool, so a caller that needs whole-node
690    /// detector coverage walks [`Self::detector_bundles`] as well.
691    pub(crate) fn stored_parities(&self) -> impl Iterator<Item = &crate::NodeDetectorParity> {
692        self.detectors
693            .iter()
694            .map(|detector| &detector.parity)
695            .chain(self.restarts.iter().map(|restart| &restart.parity))
696            .chain(self.guards.iter().flat_map(|guard| {
697                guard
698                    .detector_parities
699                    .iter()
700                    .chain(&guard.restart_parities)
701                    .map(|(_, parity)| parity)
702            }))
703    }
704}
705
706impl BloqNode {
707    pub(crate) fn kind_name(&self) -> &'static str {
708        match &self.kind {
709            BloqNodeKind::Quantum(_) => "Quantum",
710            BloqNodeKind::Region(region) => region.kind_name(),
711            BloqNodeKind::Classical(classical) => match classical.as_ref() {
712                ClassicalNode::Compute { .. } => "Compute",
713                ClassicalNode::Observable { .. } => "Observable",
714                ClassicalNode::Discard { .. } => "Discard",
715            },
716        }
717    }
718
719    /// Create a quantum node for a source block component.
720    pub fn from_members(members: Vec<SourceBlockRef>) -> Self {
721        Self::new_quantum(NodeProvenance::BlockComponent { members })
722    }
723
724    /// Create a quantum node for a temporal pipe.
725    pub fn from_temporal_pipe(pipe: TemporalPipeRef) -> Self {
726        Self::new_quantum(NodeProvenance::TemporalPipe { pipe })
727    }
728
729    /// A memory-round padding node on `pipe`'s seam, waiting `rounds`
730    /// syndrome-extraction rounds.
731    pub fn memory_padding(pipe: TemporalPipeRef, rounds: u32) -> Self {
732        Self::new_quantum(NodeProvenance::MemoryPadding { pipe, rounds })
733    }
734
735    fn new_quantum(provenance: NodeProvenance) -> Self {
736        Self::quantum(QuantumNode::default()).with_provenance(provenance)
737    }
738
739    /// A template-backed quantum node with no provenance. Its payload shares
740    /// across snapshots until [`Self::expect_quantum_mut`] edits it.
741    pub fn quantum(quantum: QuantumNode) -> Self {
742        Self {
743            kind: BloqNodeKind::Quantum(quantum.into()),
744            provenance: NodeProvenance::None,
745            activation: None,
746        }
747    }
748
749    /// A control-flow region node ([`RegionNode`]), with no provenance.
750    pub fn region(region: RegionNode) -> Self {
751        Self {
752            kind: BloqNodeKind::Region(region),
753            provenance: NodeProvenance::None,
754            activation: None,
755        }
756    }
757
758    /// A classical-dataflow node ([`ClassicalNode`]), with no provenance.
759    /// Lowering stamps the real provenance ([`NodeProvenance::Generator`] /
760    /// [`NodeProvenance::Action`] / [`NodeProvenance::OutputFrame`]) via
761    /// [`Self::with_provenance`] where it knows the origin.
762    pub fn classical(node: impl Into<Arc<ClassicalNode>>) -> Self {
763        Self {
764            kind: BloqNodeKind::Classical(node.into()),
765            provenance: NodeProvenance::None,
766            activation: None,
767        }
768    }
769
770    /// The same node with its provenance replaced — builder-style, for lowering
771    /// sites that know what a classical node realizes.
772    pub fn with_provenance(mut self, provenance: NodeProvenance) -> Self {
773        self.provenance = provenance;
774        self
775    }
776
777    /// The node's quantum payload, **panicking** on a non-quantum node (the
778    /// rustc `expect_*` convention for panicking accessors). Most passes only
779    /// ever see quantum nodes (lowering produces no others on the static
780    /// path), so they call this directly; a pass that must tolerate other
781    /// kinds uses [`Self::try_quantum`] instead.
782    ///
783    /// # Panics
784    ///
785    /// Panics when this node is classical or a region.
786    pub fn expect_quantum(&self) -> &QuantumNode {
787        self.try_quantum()
788            .expect("expect_quantum() called on a non-quantum BloqNode")
789    }
790
791    /// Mutable twin of [`Self::expect_quantum`]; detaches shared quantum data.
792    /// Panics on a non-quantum node.
793    ///
794    /// # Panics
795    ///
796    /// Panics when this node is classical or a region.
797    pub fn expect_quantum_mut(&mut self) -> &mut QuantumNode {
798        match &mut self.kind {
799            BloqNodeKind::Quantum(quantum) => Arc::make_mut(quantum),
800            BloqNodeKind::Classical(_) | BloqNodeKind::Region(_) => {
801                panic!("expect_quantum_mut() called on a non-quantum BloqNode")
802            }
803        }
804    }
805
806    /// The node's quantum payload, or `None` for a non-quantum (classical or
807    /// region) node.
808    pub fn try_quantum(&self) -> Option<&QuantumNode> {
809        match &self.kind {
810            BloqNodeKind::Quantum(quantum) => Some(quantum),
811            BloqNodeKind::Classical(_) | BloqNodeKind::Region(_) => None,
812        }
813    }
814
815    /// The node's classical payload, or `None` for a non-classical node.
816    pub fn try_classical(&self) -> Option<&ClassicalNode> {
817        self.kind.try_classical()
818    }
819
820    /// Mutable classical payload, detaching a shared definition before editing.
821    pub fn try_classical_mut(&mut self) -> Option<&mut ClassicalNode> {
822        self.kind.try_classical_mut()
823    }
824
825    /// The node's region payload, or `None` for a non-region node.
826    pub fn try_region(&self) -> Option<&RegionNode> {
827        match &self.kind {
828            BloqNodeKind::Region(region) => Some(region),
829            BloqNodeKind::Quantum(_) | BloqNodeKind::Classical(_) => None,
830        }
831    }
832
833    /// Source block members, or an empty slice for another provenance kind.
834    pub fn block_members(&self) -> &[SourceBlockRef] {
835        match &self.provenance {
836            NodeProvenance::BlockComponent { members } => members,
837            _ => &[],
838        }
839    }
840
841    /// The spliced node's wait duration in syndrome-extraction rounds, or
842    /// `None` for anything that is not a wait on a seam.
843    ///
844    pub fn memory_rounds(&self) -> Option<u32> {
845        match &self.provenance {
846            NodeProvenance::MemoryPadding { rounds, .. } => Some(*rounds),
847            _ => None,
848        }
849    }
850
851    /// The node's spatial layer on the doubled-z axis: block layers land on
852    /// even values (`2z`) and the pipe seams between them on odd values
853    /// (`2z + 1`), so temporally adjacent blocks and their connecting seam
854    /// stay strictly ordered. Classical, frame, and action nodes have no
855    /// spatial layer and report `0`.
856    pub fn layer(&self) -> i64 {
857        match &self.provenance {
858            NodeProvenance::BlockComponent { members } => {
859                2 * i64::from(members.iter().map(|member| member.pos.z).min().unwrap_or(0))
860            }
861            NodeProvenance::TemporalPipe { pipe } | NodeProvenance::MemoryPadding { pipe, .. } => {
862                2 * i64::from(pipe.src.z.min(pipe.dst.z)) + 1
863            }
864            // Spec rule SEM-SPATIAL-PORT: positionless boundary layer.
865            NodeProvenance::SpatialPortSubstitution { source, role } => {
866                2 * i64::from(source.z) + if role.has_input_boundary() { -1 } else { 1 }
867            }
868            // Classical/frame/action nodes have no spatial layer of their own.
869            NodeProvenance::Generator { .. }
870            | NodeProvenance::Action { .. }
871            | NodeProvenance::BranchSelector { .. }
872            | NodeProvenance::OutputFrame { .. }
873            | NodeProvenance::None => 0,
874        }
875    }
876}
877
878#[cfg(test)]
879mod tests {
880    use glam::ivec3;
881
882    use super::*;
883
884    /// [`ClassicalExpr::replace_inputs`] on a copy, so an assertion can compare
885    /// the mapped expression against the original.
886    fn map_inputs(expr: &ClassicalExpr, f: &mut impl FnMut(u32) -> ClassicalExpr) -> ClassicalExpr {
887        let mut mapped = expr.clone();
888        mapped.replace_inputs(f);
889        mapped
890    }
891
892    #[test]
893    fn affine_compaction_preserves_unknowns_order_and_substitution() {
894        use ClassicalExpr::{Const, In, Not, Parity, Xor};
895        let tree = Xor(Box::new([In(2), Not(Box::new(In(0))), Const(true), In(2)]));
896        let mut compact = tree.clone();
897        compact.compact_affine();
898        assert_eq!(
899            compact,
900            Parity {
901                inputs: Box::new([2, 0, 2]),
902                constant: false
903            }
904        );
905        assert_eq!(map_inputs(&compact, &mut In), compact);
906        for a in [None, Some(false), Some(true)] {
907            for b in [None, Some(false), Some(true)] {
908                let mut input = |slot| if slot == 0 { a } else { b };
909                assert_eq!(compact.eval(&mut input), tree.eval(&mut input));
910                for expr in [&compact, &tree] {
911                    let mut visited = Vec::new();
912                    let mapped = map_inputs(expr, &mut |slot| {
913                        visited.push(slot);
914                        Not(Box::new(In(slot)))
915                    });
916                    assert_eq!(visited, [2, 0, 2]);
917                    assert_eq!(
918                        mapped.eval(&mut input),
919                        tree.eval(&mut |slot| input(slot).map(|bit| !bit))
920                    );
921                }
922            }
923        }
924    }
925
926    #[test]
927    fn doubled_layers_cover_the_full_source_coordinate_range() {
928        let block = BloqNode::from_members(vec![SourceBlockRef {
929            pos: ivec3(0, 0, i32::MAX),
930        }]);
931        let pipe = BloqNode::from_temporal_pipe(TemporalPipeRef {
932            src: ivec3(0, 0, i32::MAX - 1),
933            dst: ivec3(0, 0, i32::MAX),
934            hadamard: true,
935        });
936
937        assert_eq!(block.layer(), 2 * i64::from(i32::MAX));
938        assert_eq!(pipe.layer(), 2 * i64::from(i32::MAX - 1) + 1);
939    }
940    #[test]
941    fn operand_lists_and_selection_preserve_strict_unknowns_and_input_order() {
942        use ClassicalExpr::{And, In, Or, Select, Xor};
943        let expressions = [
944            Xor(Box::new([In(0), In(1), In(2)])),
945            And(Box::new([In(0), In(1), In(2)])),
946            Or(Box::new([In(0), In(1), In(2)])),
947            Select(Box::new([In(0), In(1), In(2)])),
948        ];
949        for a in [None, Some(false), Some(true)] {
950            for b in [None, Some(false), Some(true)] {
951                for c in [None, Some(false), Some(true)] {
952                    let expected = a.zip(b).zip(c).map(|((a, b), c)| {
953                        [a ^ b ^ c, a & b & c, a | b | c, if a { c } else { b }]
954                    });
955                    for (index, expr) in expressions.iter().enumerate() {
956                        assert_eq!(
957                            expr.eval(&mut |slot| [a, b, c][slot as usize]),
958                            expected.map(|bits| bits[index])
959                        );
960                    }
961                    for literal in 0..27 {
962                        let operands: [_; 3] = std::array::from_fn(|slot| {
963                            match literal / 3usize.pow(slot as u32) % 3 {
964                                0 => In(slot as u32),
965                                value => ClassicalExpr::Const(value == 2),
966                            }
967                        });
968                        let raw = Select(Box::new(operands.clone()));
969                        let [condition, low, high] = operands;
970                        let compact = ClassicalExpr::select(condition, low, high);
971                        assert_eq!(
972                            compact.eval(&mut |slot| [a, b, c][slot as usize]),
973                            raw.eval(&mut |slot| [a, b, c][slot as usize])
974                        );
975                    }
976                }
977            }
978        }
979        for expr in &expressions {
980            let mut visited = Vec::new();
981            let mapped = map_inputs(expr, &mut |slot| {
982                visited.push(slot);
983                ClassicalExpr::Not(Box::new(In(2 - slot)))
984            });
985            assert_eq!(visited, [0, 1, 2]);
986            visited.clear();
987            mapped.for_each_input(&mut |slot| visited.push(slot));
988            assert_eq!(visited, [2, 1, 0]);
989        }
990        assert!(expressions[0].is_linear());
991        assert!(expressions[1..].iter().all(|expr| !expr.is_linear()));
992        for (expr, expected) in [
993            (Xor(Box::new([])), false),
994            (And(Box::new([])), true),
995            (Or(Box::new([])), false),
996        ] {
997            assert_eq!(
998                expr.eval(&mut |_| panic!("empty expression reads no input")),
999                Some(expected)
1000            );
1001        }
1002        for expr in [
1003            Xor(Box::new([In(0)])),
1004            And(Box::new([In(0)])),
1005            Or(Box::new([In(0)])),
1006        ] {
1007            for bit in [None, Some(false), Some(true)] {
1008                assert_eq!(expr.eval(&mut |_| bit), bit);
1009            }
1010        }
1011        #[cfg(target_pointer_width = "64")]
1012        assert_eq!(std::mem::size_of::<ClassicalExpr>(), 24);
1013    }
1014}