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}