Skip to main content

bloq_ir/
stats.rs

1//! Structural inventory of a program's stored IR.
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use crate::{Bloq, BloqEdge, BloqNodeKind, ClassicalNode};
7
8/// Structural counts across every graph level, including guarded and RUS bodies.
9///
10/// These describe stored IR without inspecting physical circuit payloads,
11/// expanding fixed repeats, selecting guards, or multiplying RUS attempts.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct BloqStats {
14    /// Templates in the compiled pool, including unreferenced templates.
15    pub template_count: usize,
16    /// Nodes across all graph levels, including region containers.
17    pub node_count: usize,
18    /// Edges across all graph levels.
19    pub edge_count: usize,
20    /// Node counts by concrete variant name, including zero-count types.
21    pub node_counts: BTreeMap<&'static str, usize>,
22    /// Edge counts by concrete variant, including zero-count types.
23    pub edge_counts: BTreeMap<&'static str, usize>,
24    /// Whether execution structure is fixed: no regions, activation, guarded
25    /// membership/seams, or shot discard. Ordinary classical readout/decoder
26    /// computation and fixed circuit repeats do not make structure dynamic.
27    /// This does not certify validity or backend emittability.
28    pub is_static: bool,
29}
30
31impl fmt::Display for BloqStats {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        writeln!(f, "Bloq IR")?;
34        writeln!(f, "├── Compiled Templates: {}", self.template_count)?;
35        for (label, count, types) in [
36            ("Nodes", self.node_count, &self.node_counts),
37            ("Edges", self.edge_count, &self.edge_counts),
38        ] {
39            writeln!(f, "├── {label}: {count}")?;
40            for (index, (kind, count)) in types.iter().enumerate() {
41                let branch = if index + 1 == types.len() {
42                    "└──"
43                } else {
44                    "├──"
45                };
46                writeln!(f, "│   {branch} {kind}: {count}")?;
47            }
48        }
49        write!(
50            f,
51            "└── Static: {}",
52            if self.is_static { "Yes" } else { "No" }
53        )
54    }
55}
56
57/// Failure to inventory stored IR; this does not certify well-formedness.
58#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
59#[non_exhaustive]
60pub enum BloqStatsError {
61    /// A structural count cannot be represented by `usize`.
62    #[error("structural IR statistics count overflows usize")]
63    CountOverflow,
64}
65
66fn add(total: &mut usize, count: usize) -> Result<(), BloqStatsError> {
67    *total = total
68        .checked_add(count)
69        .ok_or(BloqStatsError::CountOverflow)?;
70    Ok(())
71}
72
73impl Bloq {
74    /// Inventory stored IR structure without inspecting physical circuit payloads.
75    ///
76    /// Unlike [`Self::node_count`] and [`Self::edge_count`], counts include all
77    /// nested graph levels and concrete node/edge kinds. Every stored region
78    /// body contributes once, independently of guards and retry attempts.
79    /// Static means fixed execution structure: no regions, node activation,
80    /// conditional quantum membership/seams, or shot discard. Classical
81    /// computation/readout/decoding and fixed circuit repeats alone remain static.
82    /// This is neither the explicit [`Self::validate`] audit nor a guarantee
83    /// that a particular backend can emit the program.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`BloqStatsError::CountOverflow`] if a structural total overflows.
88    pub fn stats(&self) -> Result<BloqStats, BloqStatsError> {
89        let mut stats = BloqStats {
90            template_count: self.templates().len(),
91            node_count: 0,
92            edge_count: 0,
93            node_counts: [
94                "Quantum",
95                "Compute",
96                "Observable",
97                "Discard",
98                "RepeatUntilSuccess",
99            ]
100            .into_iter()
101            .map(|kind| (kind, 0))
102            .collect(),
103            edge_counts: [("Quantum", 0), ("Value", 0), ("Compose", 0), ("Order", 0)].into(),
104            is_static: true,
105        };
106        for (_, level) in self.levels() {
107            add(&mut stats.node_count, level.node_count())?;
108            add(&mut stats.edge_count, level.edge_count())?;
109            for (_, node) in level.nodes() {
110                stats.is_static &= node.activation.is_none();
111                stats.is_static &= match &node.kind {
112                    BloqNodeKind::Quantum(quantum) => quantum.guards.is_empty(),
113                    BloqNodeKind::Classical(classical) => {
114                        !matches!(classical.as_ref(), ClassicalNode::Discard { .. })
115                    }
116                    BloqNodeKind::Region(_) => false,
117                };
118                add(stats.node_counts.entry(node.kind_name()).or_default(), 1)?;
119            }
120            for edge in level.edges() {
121                let kind = match edge.edge {
122                    BloqEdge::Quantum(quantum) => {
123                        stats.is_static &= quantum.guard.is_none();
124                        "Quantum"
125                    }
126                    BloqEdge::Value { .. } => "Value",
127                    BloqEdge::Compose { .. } => "Compose",
128                    BloqEdge::Order => "Order",
129                };
130                add(stats.edge_counts.entry(kind).or_default(), 1)?;
131            }
132        }
133        Ok(stats)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::circuit::{CircuitBody, CoordCircuit, Op, PauliBasis};
141    use crate::lowering::{BloqTemplate, TemplateInstance, TemplateInstanceId};
142    use crate::{
143        BloqNode, ClassicalExpr, ObservableOutput, QuantumGuard, RegionNode, SubGraph, ValueRef,
144    };
145    use glam::ivec2;
146
147    #[test]
148    fn structural_stats_count_nested_kinds_without_inspecting_or_expanding_circuits() {
149        let mut circuit = CoordCircuit::new();
150        circuit.measure(PauliBasis::Z, [ivec2(1, 0)]);
151        let repeated = circuit.add_body(CircuitBody::new());
152        circuit
153            .body_mut(circuit.entry_body())
154            .unwrap()
155            .ops_mut()
156            .push(Op::Repeat {
157                body: repeated,
158                repetitions: u32::MAX,
159            });
160        let mut program = Bloq::new();
161        let template = program.add_template(BloqTemplate::new(circuit));
162        let mut body = SubGraph::new();
163        let mut node = BloqNode::from_members(vec![]);
164        node.expect_quantum_mut()
165            .instances
166            .push(TemplateInstance::new(
167                TemplateInstanceId(0),
168                template,
169                ivec2(i32::MAX, 0),
170            ));
171        let a = body.add_node(node);
172        let b = body.add_node(BloqNode::classical(ClassicalNode::Compute {
173            expr: ClassicalExpr::Const(false),
174        }));
175        body.add_edge(a, b, BloqEdge::Order);
176        body.add_edge(b, a, BloqEdge::value(0));
177        program.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
178            body,
179            restart_condition: ClassicalExpr::In(0),
180            restart_source: Some(ValueRef {
181                node: b,
182                output: ObservableOutput::Corrected,
183            }),
184        }));
185        let before = program.to_binary();
186        assert!(
187            program.qubit_count().is_err(),
188            "physical coordinate overflow does not block structural reads"
189        );
190        let stats = program.stats().unwrap();
191        assert_eq!(stats.node_count, 3);
192        assert_eq!(stats.node_counts.values().sum::<usize>(), stats.node_count);
193        assert_eq!(stats.node_counts["Compute"], 1);
194        assert_eq!(stats.node_counts["Quantum"], 1);
195        assert_eq!(stats.node_counts["RepeatUntilSuccess"], 1);
196        assert_eq!(stats.edge_count, 2);
197        assert_eq!(
198            stats.edge_counts,
199            [("Compose", 0), ("Order", 1), ("Quantum", 0), ("Value", 1)].into()
200        );
201        assert_eq!(stats.template_count, 1);
202        assert!(!stats.is_static);
203        assert_eq!(
204            stats.to_string(),
205            "Bloq IR\n├── Compiled Templates: 1\n├── Nodes: 3\n│   ├── Compute: 1\n│   ├── Discard: 0\n│   ├── Observable: 0\n│   ├── Quantum: 1\n│   └── RepeatUntilSuccess: 1\n├── Edges: 2\n│   ├── Compose: 0\n│   ├── Order: 1\n│   ├── Quantum: 0\n│   └── Value: 1\n└── Static: No"
206        );
207        assert_eq!(program.node_count(), 1, "existing count remains top-level");
208        assert_eq!(program.to_binary(), before, "statistics preserve stored IR");
209        let mut count = usize::MAX;
210        assert_eq!(add(&mut count, 1), Err(BloqStatsError::CountOverflow));
211    }
212
213    #[test]
214    fn static_classification_tracks_execution_structure_not_classical_values() {
215        let mut program = Bloq::new();
216        assert!(program.stats().unwrap().is_static);
217        let readout = program.add_node(BloqNode::classical(ClassicalNode::observable(0)));
218        for classical in [
219            ClassicalNode::Compute {
220                expr: ClassicalExpr::And(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
221            },
222            ClassicalNode::observable_fragment(Vec::new(), Vec::new()),
223        ] {
224            program.add_node(BloqNode::classical(classical));
225        }
226        assert!(
227            program.stats().unwrap().is_static,
228            "classical computation has fixed structure"
229        );
230        program.node_mut(readout).unwrap().activation = Some(0);
231        assert!(!program.stats().unwrap().is_static);
232        program.node_mut(readout).unwrap().activation = None;
233        let discard = program.add_node(BloqNode::classical(ClassicalNode::Discard {
234            condition: ClassicalExpr::Const(false),
235        }));
236        assert!(
237            !program.stats().unwrap().is_static,
238            "stored discard remains dynamic even with constant predicate"
239        );
240        program.remove_node(discard);
241        let a = program.add_node(BloqNode::from_members(vec![]));
242        let b = program.add_node(BloqNode::from_members(vec![]));
243        program.add_edge(a, b, BloqEdge::quantum(vec![]));
244        assert!(program.stats().unwrap().is_static);
245        program
246            .node_mut(a)
247            .unwrap()
248            .expect_quantum_mut()
249            .guards
250            .push(QuantumGuard::default());
251        assert!(!program.stats().unwrap().is_static);
252        program
253            .node_mut(a)
254            .unwrap()
255            .expect_quantum_mut()
256            .guards
257            .clear();
258        let mut guarded = BloqEdge::quantum(vec![]);
259        let BloqEdge::Quantum(edge) = &mut guarded else {
260            unreachable!()
261        };
262        edge.guard = Some(ValueRef {
263            node: readout,
264            output: ObservableOutput::Corrected,
265        });
266        program.add_edge(b, a, guarded);
267        assert!(!program.stats().unwrap().is_static);
268        program.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
269            restart_source: None,
270            restart_condition: ClassicalExpr::Const(false),
271            body: SubGraph::new(),
272        }));
273        assert_eq!(
274            program.stats().unwrap().node_counts["RepeatUntilSuccess"],
275            1
276        );
277    }
278}