Skip to main content

bloq_ir/
traverse.rs

1//! Recursive traversal over every nesting level of a [`Bloq`] — the primary
2//! way to read a compiled program's node graph.
3//!
4//! # The IR shape
5//!
6//! A [`Bloq`] is a *nested* graph. Its top level is a [`SubGraph`]; a
7//! [`crate::RegionNode`] (`RepeatUntilSuccess`)
8//! owns one nested body, and that body is itself a full [`SubGraph`]
9//! with the same read surface as the top. So "the program" is a tree of graph
10//! levels, and every quantum, classical, or region node lives at exactly one
11//! level.
12//!
13//! A [`crate::BloqNodeId`] is only unique *within* its level: the same
14//! numeric id can name a top-level node and a node inside a
15//! region body. The owner half of a node's identity is its [`LevelPath`] — the
16//! chain of enclosing `(region node, body)` hops from the top level down. A
17//! `(LevelPath, BloqNodeId)` pair is therefore a program-unique node address,
18//! the analogue of rustc's `OwnerId`/`ItemLocalId` split.
19//!
20//! # Choosing a traversal
21//!
22//! - [`Bloq::walk`] is the MLIR-style pre-order walk: it visits *individual
23//!   nodes*, top-down, and the callback returns a [`WalkControl`] to keep
24//!   going, prune a node's region bodies, or stop the whole walk early. Reach
25//!   for it when you want to inspect nodes and possibly skip subtrees.
26//! - [`Bloq::levels`] yields whole graph *levels* as `(LevelPath, &SubGraph)`.
27//!   Reach for it for table passes — instance collection, id allocation,
28//!   per-level edge queries — where you read a level as a unit rather than
29//!   reacting to each node.
30//!
31//! # Ordering
32//!
33//! Both traversals visit a level's nodes in **ascending [`crate::BloqNodeId`]
34//! order** — the deterministic validation/scan order, *not* the execution
35//! schedule. When you need the order a backend would emit in, call
36//! [`SubGraph::deterministic_emit_order`] on each level yourself; scan order
37//! and schedule order differ whenever an edge runs against ascending id order.
38
39use crate::{Bloq, BloqNode, BloqNodeId, BloqNodeKind, BodySelector, SubGraph};
40
41/// One hop of a [`LevelPath`]: the region descended through and its selected
42/// body.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
44pub struct LevelSegment {
45    /// The enclosing region node's id in its parent level.
46    pub region: BloqNodeId,
47    /// The selected body of the region.
48    pub body: BodySelector,
49}
50
51/// Path from the top level to one graph level: the chain of enclosing region
52/// body hops. Empty = the top level.
53#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize)]
54pub struct LevelPath {
55    segments: Vec<LevelSegment>,
56}
57
58impl LevelPath {
59    /// The top-level ancestor of a node at this level: the outermost
60    /// enclosing region for a nested level, else `local` itself.
61    ///
62    /// Because ids are level-local, a node's own `local` id
63    /// is only a valid key for [`Bloq::top`]/[`SubGraph::node`] when the node
64    /// *is* at the top level. For a node inside a region body, this returns the
65    /// id of the outermost enclosing region — the one node on the walk from the
66    /// top level down to it that a top-level lookup can resolve. Passing a
67    /// body-local `local` id straight to a top-level lookup would silently hit
68    /// the wrong node (or none); route it through here first.
69    pub fn top_level_ancestor(&self, local: BloqNodeId) -> BloqNodeId {
70        self.segments
71            .first()
72            .map_or(local, |segment| segment.region)
73    }
74
75    /// The region-body hops from the top level down, outermost first.
76    #[must_use]
77    pub fn segments(&self) -> &[LevelSegment] {
78        &self.segments
79    }
80
81    /// Whether this path identifies the top level.
82    #[must_use]
83    pub fn is_top_level(&self) -> bool {
84        self.segments.is_empty()
85    }
86
87    /// Whether this path is `prefix` or lies inside it.
88    #[must_use]
89    pub fn starts_with(&self, prefix: &Self) -> bool {
90        self.segments.starts_with(&prefix.segments)
91    }
92
93    /// This path extended by descending into `region`'s `body`.
94    #[must_use]
95    pub fn child(&self, region: BloqNodeId, body: BodySelector) -> Self {
96        let mut segments = self.segments.clone();
97        segments.push(LevelSegment { region, body });
98        Self { segments }
99    }
100}
101
102impl BodySelector {
103    /// The stable name of this body selector: `"body"`.
104    pub fn name(&self) -> &'static str {
105        match self {
106            BodySelector::Body => "body",
107        }
108    }
109
110    /// The inverse of [`BodySelector::name`], so a caller holding the stable
111    /// name (a serialized path, a binding-layer argument) can name a selector
112    /// without reconstructing one by scanning the program.
113    #[must_use]
114    pub fn from_name(name: &str) -> Option<Self> {
115        match name {
116            "body" => Some(BodySelector::Body),
117            _ => None,
118        }
119    }
120}
121
122/// Flow control returned from a [`Bloq::walk`] callback (MLIR `WalkResult`):
123/// whether to descend, prune, or stop after the current node.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum WalkControl {
126    /// Keep walking; descend into this node's region bodies.
127    Continue,
128    /// Keep walking, but do not descend into this node's region bodies.
129    SkipBodies,
130    /// Abort the whole walk.
131    Stop,
132}
133
134/// Context for one node visited by [`Bloq::walk`].
135///
136/// This carries the program (for templates), the node's own graph level (for edge queries),
137/// the level's path (for global identity), and the node itself.
138///
139/// `id` is level-local, so `(path, id)` together are the node's program-unique
140/// address; `id` alone only keys into the top level (see
141/// [`LevelPath::top_level_ancestor`]). Use `level` to run edge queries — e.g.
142/// [`level.incoming(id)`](SubGraph::incoming) — against the graph the node
143/// actually lives in.
144#[derive(Debug, Clone, Copy)]
145pub struct NodeCx<'a> {
146    /// The whole program, e.g. for resolving [`crate::TemplateId`]s.
147    pub bloq: &'a Bloq,
148    /// The graph level this node lives at — the target for edge queries.
149    pub level: &'a SubGraph,
150    /// The owner path to `level`; empty at the top level.
151    pub path: &'a LevelPath,
152    /// The node's level-local id (unique only within `level`).
153    pub id: BloqNodeId,
154    /// The visited node.
155    pub node: &'a BloqNode,
156}
157
158impl Bloq {
159    /// Pre-order depth-first walk over every node at every nesting level, in
160    /// ascending node-id order within a level (the validation/scan order —
161    /// use [`SubGraph::deterministic_emit_order`] per level when you need the
162    /// schedule). Returns [`WalkControl::Stop`] if the callback stopped the
163    /// walk, else [`WalkControl::Continue`].
164    ///
165    /// The callback receives a [`NodeCx`] for each visited node and returns a
166    /// [`WalkControl`]: [`Continue`](WalkControl::Continue) to descend into the
167    /// node's region bodies (if any), [`SkipBodies`](WalkControl::SkipBodies)
168    /// to visit the node but prune its bodies, or [`Stop`](WalkControl::Stop)
169    /// to abort the whole walk.
170    ///
171    /// # Examples
172    ///
173    /// In real code a [`Bloq`] comes from the compiler (`bloq_compile`), but
174    /// that crate depends on this one, so — to keep this doctest free of a
175    /// dependency cycle — we parse an equivalent program from the `.bloqir`
176    /// text format instead. It is a single quantum node feeding a
177    /// `RepeatUntilSuccess` region (whose body holds another quantum node and
178    /// an observable fragment), then a terminal observable — the shape a magic-state
179    /// (`T`) block lowers to.
180    ///
181    /// ```
182    /// use bloq_ir::{Bloq, BloqNodeKind, WalkControl};
183    ///
184    /// let program = Bloq::from_text(
185    ///     "\
186    /// BLOQIR 1
187    ///
188    /// template t0 {
189    ///   circuit {
190    ///     MPP X(0,0):m0
191    ///   }
192    /// }
193    ///
194    /// graph {
195    ///   n0 quantum {
196    ///     instance i0 t0 @ (0,0)
197    ///   }
198    ///   n1 rus in0 source n0 {
199    ///     body {
200    ///       n0 quantum {
201    ///         instance i1 t0 @ (2,0)
202    ///       }
203    ///       n1 observable fragment measurements i1:m0 from generator 0
204    ///       n0 -> n1 order
205    ///     }
206    ///   }
207    ///   n2 observable 0
208    ///   n0 -> n1 order
209    ///   n1 -> n2 order
210    /// }
211    /// ",
212    /// )
213    /// .expect("valid .bloqir text");
214    ///
215    /// // A full walk visits every node at every level, matching on kind.
216    /// let (mut quantum, mut classical, mut region) = (0, 0, 0);
217    /// program.walk(|cx| {
218    ///     match cx.node.kind {
219    ///         BloqNodeKind::Quantum(_) => quantum += 1,
220    ///         BloqNodeKind::Classical(_) => classical += 1,
221    ///         BloqNodeKind::Region(_) => region += 1,
222    ///     }
223    ///     WalkControl::Continue
224    /// });
225    /// // Two quantum (top-level + in-body), two classical (in-body fragment +
226    /// // terminal observable), one region.
227    /// assert_eq!((quantum, classical, region), (2, 2, 1));
228    ///
229    /// // `SkipBodies` prunes region bodies: the two in-body nodes are skipped,
230    /// // leaving only the three top-level nodes.
231    /// let mut top_level = 0;
232    /// program.walk(|cx| {
233    ///     top_level += 1;
234    ///     match cx.node.kind {
235    ///         BloqNodeKind::Region(_) => WalkControl::SkipBodies,
236    ///         _ => WalkControl::Continue,
237    ///     }
238    /// });
239    /// assert_eq!(top_level, 3);
240    ///
241    /// // `Stop` aborts early. Here we halt at the first region and report it.
242    /// let mut region_path = None;
243    /// let control = program.walk(|cx| {
244    ///     if matches!(cx.node.kind, BloqNodeKind::Region(_)) {
245    ///         region_path = Some(cx.path.clone());
246    ///         WalkControl::Stop
247    ///     } else {
248    ///         WalkControl::Continue
249    ///     }
250    /// });
251    /// assert_eq!(control, WalkControl::Stop);
252    /// // The region is at the top level, so its owner path is empty.
253    /// assert!(region_path.expect("stopped at the region").segments().is_empty());
254    /// ```
255    pub fn walk(&self, mut f: impl FnMut(NodeCx<'_>) -> WalkControl) -> WalkControl {
256        walk_level(self, self.top(), &mut LevelPath::default(), &mut f)
257    }
258
259    /// Every graph level, pre-order top-down: the top level, then each region
260    /// body. For whole-program table passes (instance collection, id
261    /// allocation) that read levels rather than individual nodes.
262    ///
263    /// Each item is `(LevelPath, &SubGraph)`: the path identifies the level
264    /// (empty for the top level), and the [`SubGraph`] exposes the level's full
265    /// read surface — [`nodes`](SubGraph::nodes), [`edges`](SubGraph::edges),
266    /// [`deterministic_emit_order`](SubGraph::deterministic_emit_order), etc.
267    ///
268    /// # Examples
269    ///
270    /// A per-level table pass that sums quantum nodes across the whole program
271    /// (see [`Bloq::walk`] for the `.bloqir` program used here):
272    ///
273    /// ```
274    /// use bloq_ir::{Bloq, BloqNodeKind};
275    /// # let program = Bloq::from_text(
276    /// #     "\
277    /// # BLOQIR 1
278    /// #
279    /// # template t0 {
280    /// #   circuit {
281    /// #     MPP X(0,0):m0
282    /// #   }
283    /// # }
284    /// #
285    /// # graph {
286    /// #   n0 quantum {
287    /// #     instance i0 t0 @ (0,0)
288    /// #   }
289    /// #   n1 rus in0 source n0 {
290    /// #     body {
291    /// #       n0 quantum {
292    /// #         instance i1 t0 @ (2,0)
293    /// #       }
294    /// #       n1 observable fragment measurements i1:m0 from generator 0
295    /// #       n0 -> n1 order
296    /// #     }
297    /// #   }
298    /// #   n2 observable 0
299    /// #   n0 -> n1 order
300    /// #   n1 -> n2 order
301    /// # }
302    /// # ",
303    /// # )
304    /// # .expect("valid .bloqir text");
305    ///
306    /// let mut level_count = 0;
307    /// let mut quantum_total = 0;
308    /// for (path, level) in program.levels() {
309    ///     level_count += 1;
310    ///     let quantum = level
311    ///         .nodes()
312    ///         .filter(|(_, node)| matches!(node.kind, BloqNodeKind::Quantum(_)))
313    ///         .count();
314    ///     quantum_total += quantum;
315    ///     // `path` is the level's identity: empty at the top, one hop inside
316    ///     // the region body.
317    ///     assert!(path.segments().len() <= 1);
318    /// }
319    /// // Two levels (top + the region body), one quantum node in each.
320    /// assert_eq!(level_count, 2);
321    /// assert_eq!(quantum_total, 2);
322    /// ```
323    pub fn levels(&self) -> impl Iterator<Item = (LevelPath, &SubGraph)> {
324        let mut levels = Vec::new();
325        collect_levels(self.top(), &mut LevelPath::default(), &mut levels);
326        levels.into_iter()
327    }
328
329    /// The graph level identified by `path`, or `None` if a hop is stale or
330    /// names the wrong region body.
331    #[must_use]
332    pub fn level_at(&self, path: &LevelPath) -> Option<&SubGraph> {
333        level_at_segments(self.top(), path.segments())
334    }
335
336    pub(crate) fn level_at_segments(&self, segments: &[LevelSegment]) -> Option<&SubGraph> {
337        level_at_segments(self.top(), segments)
338    }
339
340    pub(crate) fn level_at_mut(&mut self, path: &LevelPath) -> Option<&mut SubGraph> {
341        level_at_segments_mut(self.top_mut(), path.segments())
342    }
343
344    pub(crate) fn level_at_segments_mut(
345        &mut self,
346        segments: &[LevelSegment],
347    ) -> Option<&mut SubGraph> {
348        level_at_segments_mut(self.top_mut(), segments)
349    }
350}
351
352fn level_at_segments<'a>(level: &'a SubGraph, segments: &[LevelSegment]) -> Option<&'a SubGraph> {
353    let Some((&LevelSegment { region, body }, rest)) = segments.split_first() else {
354        return Some(level);
355    };
356    let body = level
357        .node(region)?
358        .try_region()?
359        .bodies()
360        .find_map(|(selector, candidate)| (selector == body).then_some(candidate))?;
361    level_at_segments(body, rest)
362}
363
364fn level_at_segments_mut<'a>(
365    level: &'a mut SubGraph,
366    segments: &[LevelSegment],
367) -> Option<&'a mut SubGraph> {
368    let Some((&LevelSegment { region, body }, rest)) = segments.split_first() else {
369        return Some(level);
370    };
371    let BloqNodeKind::Region(region) = &mut level.node_mut(region)?.kind else {
372        return None;
373    };
374    let body = region
375        .bodies_mut()
376        .find_map(|(selector, candidate)| (selector == body).then_some(candidate))?;
377    level_at_segments_mut(body, rest)
378}
379
380fn walk_level(
381    bloq: &Bloq,
382    level: &SubGraph,
383    path: &mut LevelPath,
384    f: &mut impl FnMut(NodeCx<'_>) -> WalkControl,
385) -> WalkControl {
386    for (id, node) in level.nodes() {
387        match f(NodeCx {
388            bloq,
389            level,
390            path,
391            id,
392            node,
393        }) {
394            WalkControl::Stop => return WalkControl::Stop,
395            WalkControl::SkipBodies => continue,
396            WalkControl::Continue => {}
397        }
398        let Some(region) = node.try_region() else {
399            continue;
400        };
401        for (selector, body) in region.bodies() {
402            path.segments.push(LevelSegment {
403                region: id,
404                body: selector,
405            });
406            let control = walk_level(bloq, body, path, f);
407            path.segments.pop();
408            if control == WalkControl::Stop {
409                return WalkControl::Stop;
410            }
411        }
412    }
413    WalkControl::Continue
414}
415
416fn collect_levels<'a>(
417    level: &'a SubGraph,
418    path: &mut LevelPath,
419    out: &mut Vec<(LevelPath, &'a SubGraph)>,
420) {
421    out.push((path.clone(), level));
422    for (id, node) in level.nodes() {
423        let Some(region) = node.try_region() else {
424            continue;
425        };
426        for (selector, body) in region.bodies() {
427            path.segments.push(LevelSegment {
428                region: id,
429                body: selector,
430            });
431            collect_levels(body, path, out);
432            path.segments.pop();
433        }
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn level_path_json_preserves_scope_schema() {
443        let path = LevelPath::default().child(BloqNodeId(7), BodySelector::Body);
444        assert_eq!(
445            serde_json::to_value(path).unwrap(),
446            serde_json::json!({ "segments": [{ "region": 7, "body": "Body" }] })
447        );
448    }
449}