Skip to main content

bloq_ir/
query.rs

1//! Structural queries: the shapes a consumer would otherwise rediscover by
2//! matching on [`BloqNodeKind`](crate::BloqNodeKind)/[`RegionNode`] variants
3//! and hard-coding source-block coordinates.
4//!
5//! A compiled program has a small set of recurring landmarks — the region
6//! nodes of a given kind, the seams feeding a selection, a graph level's
7//! quantum tail, and the node realizing a named
8//! source block. Each is a structural fact the IR already knows, so it belongs
9//! here rather than in every backend and experiment that needs it.
10//!
11//! Queries that can only have one answer return [`StructureError`] when the
12//! program does not have that shape, so a program whose lowering changed fails
13//! at the query instead of silently producing a plausible wrong node.
14
15use glam::IVec3;
16use thiserror::Error;
17
18use crate::{Bloq, BloqEdge, BloqNodeId, LevelPath, RegionNode, SubGraph};
19
20/// Why a structural query found no unique answer.
21#[derive(Debug, Clone, PartialEq, Eq, Error)]
22#[non_exhaustive]
23pub enum StructureError {
24    /// A node does not have exactly one quantum input.
25    #[error("node {node:?} has {count} quantum inputs, expected exactly one")]
26    QuantumInputNotUnique {
27        /// Queried node.
28        node: BloqNodeId,
29        /// Quantum-input count.
30        count: usize,
31    },
32    /// A selection has no quantum input.
33    #[error("selection node {node:?} has no quantum input")]
34    SelectionInputMissing {
35        /// Selection node.
36        node: BloqNodeId,
37    },
38    /// A graph level has no unique quantum tail.
39    #[error(
40        "graph level has {count} quantum tails (quantum nodes with no outgoing quantum edge), expected exactly one"
41    )]
42    QuantumTailNotUnique {
43        /// Quantum-tail count.
44        count: usize,
45    },
46}
47
48/// Which [`RegionNode`] variant a structural query selects.
49///
50/// The discriminant half of [`RegionNode`], so a caller can filter regions
51/// without matching a variant it does not intend to destructure. Exhaustive
52/// like the IR data enums it mirrors (see the crate docs).
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub enum RegionKind {
55    /// Repeat-until-success region.
56    RepeatUntilSuccess,
57}
58
59impl RegionNode {
60    /// This region's variant, without its payload.
61    pub fn kind(&self) -> RegionKind {
62        RegionKind::RepeatUntilSuccess
63    }
64}
65
66/// A program-unique address of one region node: the path to its graph level
67/// plus its level-local id.
68///
69/// Both halves are needed because [`BloqNodeId`]s are level-local; see
70/// [`LevelPath`].
71#[derive(Debug, Clone, PartialEq, Eq, Hash)]
72pub struct RegionRef {
73    /// The level the region node lives at; empty for the top level.
74    pub path: LevelPath,
75    /// The region node's id within that level.
76    pub node: BloqNodeId,
77}
78
79impl RegionRef {
80    /// The region this reference names, or `None` once the program has been
81    /// edited out from under it (a stale path, or an id whose node is gone).
82    #[must_use]
83    pub fn resolve<'a>(&self, bloq: &'a Bloq) -> Option<&'a RegionNode> {
84        bloq.level_at(&self.path)?.node(self.node)?.try_region()
85    }
86}
87
88impl Bloq {
89    /// Every region node of `kind`, at every nesting level.
90    ///
91    /// Ordered by [`Bloq::levels`]: levels pre-order top-down, ascending node
92    /// id within a level. Filter on [`RegionRef::path`] being
93    /// [top-level](LevelPath::is_top_level) when only the outermost regions
94    /// matter.
95    #[must_use]
96    pub fn regions_of(&self, kind: RegionKind) -> Vec<RegionRef> {
97        self.levels()
98            .flat_map(|(path, level)| {
99                level
100                    .nodes()
101                    .filter(move |(_, node)| {
102                        node.try_region()
103                            .is_some_and(|region| region.kind() == kind)
104                    })
105                    .map(move |(node, _)| RegionRef {
106                        path: path.clone(),
107                        node,
108                    })
109            })
110            .collect()
111    }
112
113    /// The quantum seams each top-level guarded component waits on, as
114    /// `(source, selection)` pairs in ascending selection-node-id order.
115    ///
116    /// Subdividing those edges ([`Bloq::insert_memory_rounds_batch`]) delays the
117    /// component until its guard inputs are ready. Within one component, seams
118    /// are ordered by source node id. This query does not certify that an
119    /// unresolved selection supports scheduling or memory edits.
120    ///
121    /// # Errors
122    ///
123    /// Returns [`StructureError::SelectionInputMissing`] if a guarded component has no
124    /// incoming quantum edge.
125    pub fn selection_seams(&self) -> Result<Vec<(BloqNodeId, BloqNodeId)>, StructureError> {
126        let mut seams = Vec::new();
127        for (node, _) in self
128            .top()
129            .quantum_nodes()
130            .filter(|(_, quantum)| !quantum.guards.is_empty())
131        {
132            let mut sources: Vec<_> = self
133                .incoming(node)
134                .filter(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
135                .map(|edge| edge.source)
136                .collect();
137            if sources.is_empty() {
138                return Err(StructureError::SelectionInputMissing { node });
139            }
140            sources.sort_unstable();
141            seams.extend(sources.into_iter().map(|source| (source, node)));
142        }
143        Ok(seams)
144    }
145
146    /// The top-level node realizing source block `pos`, or `None` when no node
147    /// carries it.
148    ///
149    /// Lowering partitions the source blocks across
150    /// [`NodeProvenance::BlockComponent`](crate::NodeProvenance::BlockComponent)
151    /// nodes, so at most one node can match — this is a lookup, not a search
152    /// with a preferred answer.
153    #[must_use]
154    pub fn node_by_block(&self, pos: IVec3) -> Option<BloqNodeId> {
155        self.nodes().find_map(|(id, node)| {
156            node.block_members()
157                .iter()
158                .any(|member| member.pos == pos)
159                .then_some(id)
160        })
161    }
162}
163
164impl SubGraph {
165    /// The source of the single quantum edge into `node`.
166    ///
167    /// This is the seam a padding edit subdivides: every consumer that idles
168    /// before a node first has to establish that the node has exactly one
169    /// quantum predecessor to idle behind.
170    ///
171    /// # Errors
172    ///
173    /// Returns [`StructureError::QuantumInputNotUnique`] when `node` has zero
174    /// or several incoming quantum edges (a stale id reads as zero).
175    pub fn quantum_input(&self, node: BloqNodeId) -> Result<BloqNodeId, StructureError> {
176        let sources: Vec<_> = self
177            .incoming(node)
178            .filter(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
179            .map(|edge| edge.source)
180            .collect();
181        match sources[..] {
182            [source] => Ok(source),
183            _ => Err(StructureError::QuantumInputNotUnique {
184                node,
185                count: sources.len(),
186            }),
187        }
188    }
189
190    /// The unique quantum tail of this graph level: a quantum node with no
191    /// outgoing quantum edge. Classical consumers do not disqualify it.
192    ///
193    /// This query applies to top-level graphs and nested bodies alike.
194    ///
195    /// # Errors
196    ///
197    /// Returns [`StructureError::QuantumTailNotUnique`] unless exactly one
198    /// quantum node is terminal.
199    pub fn quantum_tail(&self) -> Result<BloqNodeId, StructureError> {
200        let tails: Vec<_> = self
201            .quantum_nodes()
202            .map(|(id, _)| id)
203            .filter(|&id| {
204                !self
205                    .outgoing(id)
206                    .any(|edge| matches!(edge.edge, BloqEdge::Quantum(_)))
207            })
208            .collect();
209        match tails[..] {
210            [tail] => Ok(tail),
211            _ => Err(StructureError::QuantumTailNotUnique { count: tails.len() }),
212        }
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use glam::ivec3;
219
220    use super::*;
221    use crate::BodySelector;
222
223    /// One `RepeatUntilSuccess` (its body a source node feeding an escape node
224    /// that has no quantum successor) followed by a guarded component fed by one quantum
225    /// seam — the landmark set every query here answers about.
226    fn program() -> Bloq {
227        Bloq::from_text(
228            "\
229BLOQIR 1
230
231template t0 {
232  circuit {
233    MPP X(0,0):m0
234  }
235}
236
237graph {
238  n0 quantum {
239    instance i0 t0 @ (0,0)
240    from blocks (0,0,0)
241  }
242  n1 rus in0 source n2 {
243    body {
244      n0 quantum {
245        instance i1 t0 @ (2,0)
246      }
247      n1 quantum {
248        instance i2 t0 @ (2,2)
249      }
250      n2 observable fragment measurements i2:m0 from generator 0
251      n0 -> n1 quantum (0,0,0)>(0,0,1)
252      n1 -> n2 order
253    }
254  }
255  n2 quantum {
256    instance i3 t0 @ (4,0)
257    from blocks (1,0,2)
258  }
259  n3 quantum {
260    instance i4 t0 @ (4,0)
261    guard 0 i4
262  }
263  n4 compute 1
264  n4 -> n3 value 0
265  n0 -> n2 quantum (0,0,0)>(0,0,1)
266  n2 -> n3 quantum (1,0,2)>(1,0,3)
267}
268",
269        )
270        .expect("valid .bloqir text")
271    }
272
273    #[test]
274    fn regions_of_selects_one_variant_across_levels() {
275        let program = program();
276
277        assert_eq!(
278            program.regions_of(RegionKind::RepeatUntilSuccess),
279            [RegionRef {
280                path: LevelPath::default(),
281                node: BloqNodeId(1),
282            }]
283        );
284    }
285
286    #[test]
287    fn selection_seams_report_the_edge_each_selection_waits_on() {
288        assert_eq!(
289            program().selection_seams(),
290            Ok(vec![(BloqNodeId(2), BloqNodeId(3))])
291        );
292    }
293
294    #[test]
295    fn selection_seams_report_every_quantum_input() {
296        let mut program = program();
297        program.add_edge(
298            BloqNodeId(0),
299            BloqNodeId(3),
300            BloqEdge::quantum(vec![crate::TemporalPipeRef {
301                src: ivec3(0, 0, 0),
302                dst: ivec3(0, 0, 1),
303                hadamard: false,
304            }]),
305        );
306
307        assert_eq!(
308            program.selection_seams(),
309            Ok(vec![
310                (BloqNodeId(0), BloqNodeId(3)),
311                (BloqNodeId(2), BloqNodeId(3)),
312            ])
313        );
314    }
315
316    #[test]
317    fn selection_seams_reject_a_component_without_quantum_inputs() {
318        use petgraph::visit::EdgeRef;
319        let mut program = program();
320        let edge = program
321            .graph()
322            .edges_connecting(
323                petgraph::stable_graph::NodeIndex::new(2),
324                petgraph::stable_graph::NodeIndex::new(3),
325            )
326            .next()
327            .unwrap()
328            .id();
329        program.graph_mut_internal().remove_edge(edge);
330
331        assert_eq!(
332            program.selection_seams(),
333            Err(StructureError::SelectionInputMissing {
334                node: BloqNodeId(3),
335            })
336        );
337    }
338
339    #[test]
340    fn quantum_tail_is_the_terminal_quantum_node_of_a_region_body() {
341        let program = program();
342        let body = program
343            .level_at(&LevelPath::default().child(BloqNodeId(1), BodySelector::Body))
344            .expect("rus body");
345
346        assert_eq!(body.quantum_tail(), Ok(BloqNodeId(1)));
347        assert_eq!(program.top().quantum_tail(), Ok(BloqNodeId(3)));
348    }
349
350    #[test]
351    fn quantum_tail_ignores_classical_consumers_and_requires_a_unique_tail() {
352        use crate::{BloqNode, ClassicalExpr, ClassicalNode, QuantumNode};
353
354        let mut graph = SubGraph::new();
355        assert_eq!(
356            graph.quantum_tail(),
357            Err(StructureError::QuantumTailNotUnique { count: 0 })
358        );
359        let head = graph.add_node(BloqNode::quantum(QuantumNode::default()));
360        let tail = graph.add_node(BloqNode::quantum(QuantumNode::default()));
361        let readout = graph.add_node(BloqNode::classical(ClassicalNode::Compute {
362            expr: ClassicalExpr::Const(false),
363        }));
364        graph.add_edge(head, tail, BloqEdge::quantum(vec![]));
365        graph.add_edge(tail, readout, BloqEdge::Order);
366        assert_eq!(graph.quantum_tail(), Ok(tail));
367        graph.add_node(BloqNode::quantum(QuantumNode::default()));
368        assert_eq!(
369            graph.quantum_tail(),
370            Err(StructureError::QuantumTailNotUnique { count: 2 })
371        );
372    }
373
374    #[test]
375    fn node_by_block_addresses_a_node_by_its_source_coordinates() {
376        let program = program();
377
378        assert_eq!(program.node_by_block(ivec3(1, 0, 2)), Some(BloqNodeId(2)));
379        // A block inside a region body is not a top-level member.
380        assert_eq!(program.node_by_block(ivec3(9, 9, 9)), None);
381    }
382}