Skip to main content

bloq_ir/ir/
program.rs

1use std::cmp::Reverse;
2use std::collections::{BTreeMap, BinaryHeap};
3use std::ops::Index;
4use std::sync::{Arc, OnceLock};
5
6use bloq_circuit::{CoordinateOverflowError, checked_translate_coordinate};
7use glam::IVec2;
8use petgraph::stable_graph::{NodeIndex, StableDiGraph};
9use petgraph::visit::{EdgeRef, IntoEdgeReferences, IntoNodeIdentifiers, NodeIndexable};
10
11use super::{BloqEdgeRef, PathScratch, ValueInput};
12use crate::{
13    BloqEdge, BloqNode, BloqNodeId, BloqTemplate, BloqTemplatePool, DetectorBundle,
14    DetectorBundleId, DetectorBundlePool, FxSet, LogicalInput, LogicalOutput, PipePadding,
15    QuantumNode, SubGraph, TemplateId,
16};
17
18/// A portable scalar stored in [`Bloq`] producer metadata.
19#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
20pub enum MetadataValue {
21    /// Unsigned integer metadata.
22    U64(u64),
23    /// UTF-8 string metadata.
24    String(String),
25}
26
27impl From<u32> for MetadataValue {
28    fn from(value: u32) -> Self {
29        Self::U64(u64::from(value))
30    }
31}
32
33impl From<String> for MetadataValue {
34    fn from(value: String) -> Self {
35        Self::String(value)
36    }
37}
38
39/// The compiled bloq circuit IR: a graph of quantum, classical, and region
40/// nodes produced by `bloq_compile`, consumed by emitters and the verifier.
41///
42/// The top level is a [`SubGraph`] — the same graph-level type region bodies
43/// use — so every read accessor ([`SubGraph::node`], [`SubGraph::nodes`],
44/// [`SubGraph::incoming`], …) works uniformly at any nesting level. `Bloq`
45/// delegates the common calls and adds whole-program state (templates,
46/// frames, provenance) plus recursive traversal ([`Bloq::walk`],
47/// [`Bloq::levels`]).
48///
49/// Cloning shares graph levels, quantum payloads, and templates; edits detach
50/// the affected storage. Metadata and populated layout caches are still copied.
51// `Clone` is derived: cloning a populated `sorted_layout_coords` cache is
52// sound because the cache is a pure function of the cloned graph.
53#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
54pub struct Bloq {
55    templates: BloqTemplatePool,
56    detector_bundles: DetectorBundlePool,
57    top: SubGraph,
58    /// Namespaced producer provenance. It never affects IR semantics.
59    metadata: BTreeMap<String, MetadataValue>,
60    /// Physical input-patch metadata needed by preparation hooks.
61    /// Empty for hand-built programs and inputs without a complete pair.
62    #[serde(default)]
63    pub(super) logical_inputs: Vec<LogicalInput>,
64    /// Terminal logical operators needed by output-state execution hooks.
65    /// Empty for hand-built programs and outputs without a complete pair.
66    pub(super) logical_outputs: Vec<LogicalOutput>,
67    /// Memo for [`Bloq::insert_memory_rounds`]'s padding-template
68    /// specialization: [`PaddingVariant`] -> specialized template, so padding
69    /// many seams at one wait duration shares one pool entry. A
70    /// rebuild-on-demand cache, never serialized. Pool ids are append-only;
71    /// the template-retuning edit clears this memo when circuit contents
72    /// change.
73    ///
74    /// [`PaddingVariant`]: crate::edit::PaddingVariant
75    #[serde(skip)]
76    pub(crate) specialized_padding: crate::FxMap<crate::edit::PaddingVariant, TemplateId>,
77    /// Derived cache, rebuilt on demand — never serialized.
78    #[serde(skip)]
79    sorted_layout_coords: OnceLock<Result<Vec<IVec2>, CoordinateOverflowError>>,
80}
81
82impl Bloq {
83    /// Create an empty program.
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    /// Assemble a program from parsed parts (the text codec's constructor).
89    pub(crate) fn from_parts(
90        templates: BloqTemplatePool,
91        detector_bundles: DetectorBundlePool,
92        top: SubGraph,
93        metadata: BTreeMap<String, MetadataValue>,
94        logical_inputs: Vec<LogicalInput>,
95        logical_outputs: Vec<LogicalOutput>,
96    ) -> Self {
97        Self {
98            templates,
99            detector_bundles,
100            top,
101            metadata,
102            logical_inputs,
103            logical_outputs,
104            specialized_padding: crate::FxMap::default(),
105            sorted_layout_coords: OnceLock::new(),
106        }
107    }
108
109    /// The top graph level. Region bodies are further [`SubGraph`] levels,
110    /// reached through [`crate::RegionNode::bodies`] or [`Bloq::levels`].
111    pub fn top(&self) -> &SubGraph {
112        &self.top
113    }
114
115    /// Mutable top graph level.
116    ///
117    /// This invalidates derived layout data because arbitrary graph edits may
118    /// change the program's qubit footprint.
119    pub fn top_mut(&mut self) -> &mut SubGraph {
120        self.sorted_layout_coords = OnceLock::new();
121        &mut self.top
122    }
123
124    /// Producer-specific provenance, keyed by a namespaced stable name.
125    pub fn metadata(&self) -> &BTreeMap<String, MetadataValue> {
126        &self.metadata
127    }
128
129    /// Insert producer metadata, returning the previous value for `key`.
130    pub fn insert_metadata(
131        &mut self,
132        key: impl Into<String>,
133        value: impl Into<MetadataValue>,
134    ) -> Option<MetadataValue> {
135        self.metadata.insert(key.into(), value.into())
136    }
137
138    /// Every edge-owned memory-padding record in the program.
139    pub fn pipe_padding(&self) -> impl Iterator<Item = &PipePadding> {
140        self.levels().flat_map(|(_, level)| {
141            level
142                .edges()
143                .flat_map(|edge| edge.edge.pipes())
144                .filter_map(|seam| seam.padding.as_ref())
145        })
146    }
147
148    /// Replace the padding provenance on the unique top-level quantum edge
149    /// `from -> to`, one record per pipe in pipe order. Returns `false` when
150    /// that edge is missing, ambiguous, or carries a different pipe count.
151    ///
152    /// This is public only for the compiler crate; ordinary edits consume the
153    /// provenance through [`Bloq::insert_memory_rounds`].
154    #[doc(hidden)]
155    pub fn set_quantum_edge_padding(
156        &mut self,
157        from: BloqNodeId,
158        to: BloqNodeId,
159        padding: Vec<PipePadding>,
160    ) -> bool {
161        let edge_id = {
162            let mut edges = self
163                .graph()
164                .edges_connecting(
165                    NodeIndex::new(from.0 as usize),
166                    NodeIndex::new(to.0 as usize),
167                )
168                .filter(|edge| matches!(edge.weight(), BloqEdge::Quantum(_)));
169            let Some(edge) = edges.next() else {
170                return false;
171            };
172            if edges.next().is_some() {
173                return false;
174            }
175            edge.id()
176        };
177        let Some(BloqEdge::Quantum(edge)) = self.graph_mut_internal().edge_weight_mut(edge_id)
178        else {
179            unreachable!("selected a quantum edge")
180        };
181        if edge.pipes.len() != padding.len() {
182            return false;
183        }
184        for (seam, padding) in edge.pipes.iter_mut().zip(padding) {
185            seam.padding = Some(padding);
186        }
187        self.specialized_padding.clear();
188        true
189    }
190
191    /// Add an owned template and return its id.
192    ///
193    /// # Panics
194    ///
195    /// Panics before insertion if the next template id exceeds `u32`.
196    pub fn add_template(&mut self, template: BloqTemplate) -> TemplateId {
197        self.sorted_layout_coords = OnceLock::new();
198        self.templates.insert(template)
199    }
200
201    /// Add a shared template and return its id.
202    ///
203    /// # Panics
204    ///
205    /// Panics before insertion if the next template id exceeds `u32`.
206    pub fn add_shared_template(&mut self, template: Arc<BloqTemplate>) -> TemplateId {
207        self.sorted_layout_coords = OnceLock::new();
208        self.templates.insert_shared(template)
209    }
210
211    /// Program template pool.
212    pub fn templates(&self) -> &BloqTemplatePool {
213        &self.templates
214    }
215
216    /// Reusable detector bundle pool.
217    pub fn detector_bundles(&self) -> &DetectorBundlePool {
218        &self.detector_bundles
219    }
220
221    /// Add owned detector rows to the shared bundle pool.
222    ///
223    /// # Panics
224    ///
225    /// Panics before insertion if the next bundle id exceeds `u32`.
226    pub fn add_detector_bundle(&mut self, bundle: DetectorBundle) -> DetectorBundleId {
227        self.detector_bundles.insert(bundle)
228    }
229
230    /// Import an immutable shared detector bundle.
231    ///
232    /// # Panics
233    ///
234    /// Panics before insertion if the next bundle id exceeds `u32`.
235    pub fn add_shared_detector_bundle(&mut self, bundle: Arc<DetectorBundle>) -> DetectorBundleId {
236        self.detector_bundles.insert_shared(bundle)
237    }
238
239    /// Mutable template pool for this crate's in-place template edits;
240    /// invalidates the derived layout cache like the other mutators, since a
241    /// retuned template may change the program's qubit footprint.
242    pub(crate) fn templates_mut(&mut self) -> &mut BloqTemplatePool {
243        self.sorted_layout_coords = OnceLock::new();
244        &mut self.templates
245    }
246
247    /// Add a top-level node and return its id.
248    pub fn add_node(&mut self, node: BloqNode) -> BloqNodeId {
249        self.sorted_layout_coords = OnceLock::new();
250        self.top.add_node(node)
251    }
252
253    /// Adds a top-level edge; panics on a stale endpoint id, like
254    /// [`SubGraph::add_edge`].
255    pub fn add_edge(&mut self, from: BloqNodeId, to: BloqNodeId, edge: BloqEdge) {
256        self.top.add_edge(from, to, edge);
257    }
258
259    // ---- top-level mutating delegations ------------------------------------
260    //
261    // The read half of the mirror is generated by `delegate_to_top!` below.
262    // These stay hand-written because each also has to drop the derived layout
263    // cache: an edit may change the program's qubit footprint.
264
265    /// Mutable top-level node lookup.
266    pub fn node_mut(&mut self, id: BloqNodeId) -> Option<&mut BloqNode> {
267        self.sorted_layout_coords = OnceLock::new();
268        self.top.node_mut(id)
269    }
270
271    /// Remove a top-level node and its incident edges.
272    pub fn remove_node(&mut self, id: BloqNodeId) -> Option<BloqNode> {
273        self.sorted_layout_coords = OnceLock::new();
274        self.top.remove_node(id)
275    }
276
277    /// The number of top-level quantum nodes. Unlike [`Self::node_count`] this
278    /// excludes the classical `Observable` nodes, so it
279    /// stays a stable measure of lowered blocks for perf tracking.
280    pub fn quantum_node_count(&self) -> usize {
281        self.quantum_nodes().count()
282    }
283
284    // -------------------------------------------------------------------------
285
286    /// The number of distinct physical qubit coordinates in the layout.
287    ///
288    /// # Errors
289    ///
290    /// Returns [`CoordinateOverflowError`] if translating an instance qubit
291    /// exceeds the coordinate lattice.
292    pub fn qubit_count(&self) -> Result<usize, CoordinateOverflowError> {
293        Ok(self.sorted_layout_coords()?.len())
294    }
295
296    /// The program's distinct layout qubit coordinates, sorted by `(x, y)`.
297    /// Computed once and cached; the cache is cleared on any mutation.
298    ///
299    /// # Errors
300    ///
301    /// Returns [`CoordinateOverflowError`] if an instance offset would move a
302    /// template qubit outside the `i32` coordinate lattice.
303    pub fn sorted_layout_coords(&self) -> Result<&[IVec2], CoordinateOverflowError> {
304        match self
305            .sorted_layout_coords
306            .get_or_init(|| self.collect_sorted_layout_coords())
307        {
308            Ok(coords) => Ok(coords),
309            Err(error) => Err(*error),
310        }
311    }
312
313    fn collect_sorted_layout_coords(&self) -> Result<Vec<IVec2>, CoordinateOverflowError> {
314        let mut qubits = FxSet::default();
315        for (_, node) in self.top.nodes() {
316            self.extend_node_qubits(node, &mut qubits)?;
317        }
318        let mut coords = qubits.into_iter().collect::<Vec<_>>();
319        coords.sort_by_key(|coord| (coord.x, coord.y));
320        Ok(coords)
321    }
322
323    fn extend_node_qubits(
324        &self,
325        node: &BloqNode,
326        qubits: &mut FxSet<IVec2>,
327    ) -> Result<(), CoordinateOverflowError> {
328        // A region's footprint is its bodies' footprint:
329        // arm/body instances resolve against the same shared template pool, so
330        // recurse rather than reporting a region as physically empty.
331        if let Some(region) = node.try_region() {
332            for (_, body) in region.bodies() {
333                for (_, body_node) in body.nodes() {
334                    self.extend_node_qubits(body_node, qubits)?;
335                }
336            }
337            return Ok(());
338        }
339        let Some(quantum) = node.try_quantum() else {
340            return Ok(()); // classical nodes carry no qubits
341        };
342        let mut seen: FxSet<(crate::TemplateId, IVec2)> = FxSet::default();
343        for instance in &quantum.instances {
344            // Instances repeat the same (template, offset) — one round per time
345            // layer, sometimes interleaved across layers — and contribute
346            // nothing new. Track all seen pairs so a repeat never re-walks the
347            // template's qubit list, not just consecutive duplicates.
348            if !seen.insert((instance.template_id, instance.offset)) {
349                continue;
350            }
351            if let Some(template) = self.templates.get(instance.template_id) {
352                let template_qubits = template.qubits();
353                qubits.reserve(template_qubits.len());
354                for &qubit in template_qubits {
355                    qubits.insert(checked_translate_coordinate(qubit, instance.offset)?);
356                }
357            }
358        }
359        Ok(())
360    }
361
362    /// The layout qubits a node occupies: each instance's template qubits
363    /// translated by the instance offset. Instances pointing at a missing
364    /// template are skipped.
365    ///
366    /// # Errors
367    ///
368    /// Returns [`CoordinateOverflowError`] if an instance offset would move a
369    /// template qubit outside the `i32` coordinate lattice.
370    pub fn node_qubits(&self, node: &BloqNode) -> Result<FxSet<IVec2>, CoordinateOverflowError> {
371        let mut qubits = FxSet::default();
372        self.extend_node_qubits(node, &mut qubits)?;
373        Ok(qubits)
374    }
375
376    /// The number of static measurement sites in a node and its nested bodies.
377    /// RUS bodies contribute; this is not a runtime
378    /// execution count.
379    pub fn node_measurement_count(&self, node: &BloqNode) -> usize {
380        if let Some(region) = node.try_region() {
381            return region
382                .bodies()
383                .flat_map(|(_, body)| body.nodes())
384                .map(|(_, body_node)| self.node_measurement_count(body_node))
385                .sum();
386        }
387        let Some(quantum) = node.try_quantum() else {
388            return 0; // classical nodes emit no measurements
389        };
390        quantum
391            .instances
392            .iter()
393            .filter_map(|instance| self.templates.get(instance.template_id))
394            .map(|template| template.circuit.num_measurements() as usize)
395            .sum()
396    }
397
398    /// The number of static measurement sites in the program.
399    pub fn measurement_count(&self) -> usize {
400        self.top
401            .nodes()
402            .map(|(_, node)| self.node_measurement_count(node))
403            .sum()
404    }
405
406    /// Read-only raw graph access for this crate's own passes; consumers use
407    /// the id-native accessors instead (petgraph is not public API).
408    pub(crate) fn graph(&self) -> &StableDiGraph<BloqNode, BloqEdge> {
409        self.top.graph()
410    }
411
412    /// Mutable graph access for the edit module (`crate::edit`); invalidates
413    /// the cached layout since edits may change node qubit footprints.
414    pub(crate) fn graph_mut_internal(&mut self) -> &mut StableDiGraph<BloqNode, BloqEdge> {
415        self.sorted_layout_coords = OnceLock::new();
416        self.top.graph_mut()
417    }
418}
419
420/// Mirror [`SubGraph`]'s read surface onto [`Bloq`], targeting the top level.
421///
422/// These are pure forwarding calls, so hand-writing them only invited drift —
423/// `open_value_producers`/`open_bit_producers` had no `Bloq` twin at all until
424/// this list made the mirror total. `Deref<Target = SubGraph>` would collapse
425/// them further, but a `Bloq` is not a smart pointer to its top level
426/// (C-DEREF), and `DerefMut` would hand out `&mut SubGraph` implicitly at every
427/// mutating call site, hiding the layout-cache contract the mutators above
428/// spell out.
429macro_rules! delegate_to_top {
430    ($(fn $name:ident($($arg:ident: $ty:ty),* $(,)?) -> $ret:ty;)*) => {
431        impl Bloq {
432            $(
433                #[doc = concat!(
434                    "The top level's [`SubGraph::", stringify!($name), "`]. Region body \
435                     levels are reached through [`Bloq::levels`] / [`Bloq::level_at`]."
436                )]
437                pub fn $name(&self $(, $arg: $ty)*) -> $ret {
438                    self.top.$name($($arg),*)
439                }
440            )*
441        }
442    };
443}
444
445delegate_to_top! {
446    fn node(id: BloqNodeId) -> Option<&BloqNode>;
447    fn node_ids() -> impl DoubleEndedIterator<Item = BloqNodeId> + '_;
448    fn nodes() -> impl DoubleEndedIterator<Item = (BloqNodeId, &BloqNode)> + '_;
449    fn quantum_nodes() -> impl Iterator<Item = (BloqNodeId, &QuantumNode)> + '_;
450    fn node_count() -> usize;
451    fn edge_count() -> usize;
452    fn edges() -> impl Iterator<Item = BloqEdgeRef<'_>> + '_;
453    fn incoming(id: BloqNodeId) -> impl Iterator<Item = BloqEdgeRef<'_>> + '_;
454    fn outgoing(id: BloqNodeId) -> impl Iterator<Item = BloqEdgeRef<'_>> + '_;
455    fn edges_between(from: BloqNodeId, to: BloqNodeId) -> impl Iterator<Item = BloqEdgeRef<'_>> + '_;
456    fn has_path(from: BloqNodeId, to: BloqNodeId) -> bool;
457    fn path_scratch() -> PathScratch;
458    fn has_path_with_scratch(from: BloqNodeId, to: BloqNodeId, scratch: &mut PathScratch) -> bool;
459    fn value_inputs(id: BloqNodeId) -> impl Iterator<Item = ValueInput<'_>> + '_;
460    fn data_inputs(id: BloqNodeId) -> impl Iterator<Item = ValueInput<'_>> + '_;
461    fn data_consumers(id: BloqNodeId) -> impl Iterator<Item = (BloqNodeId, u32)> + '_;
462    fn compose_consumers(id: BloqNodeId) -> impl Iterator<Item = (BloqNodeId, u32)> + '_ ;
463    fn value_consumers(id: BloqNodeId) -> impl Iterator<Item = (BloqNodeId, u32)> + '_;
464    fn open_value_producers() -> impl Iterator<Item = BloqNodeId> + '_;
465    fn open_bit_producers() -> impl Iterator<Item = BloqNodeId> + '_;
466    fn value_output() -> Option<crate::ValueRef>;
467    fn boundary_outputs() -> &[BloqNodeId];
468    fn is_acyclic() -> bool;
469    fn is_acyclic_with_edges(extra: &[(BloqNodeId, BloqNodeId)]) -> bool;
470}
471
472impl Bloq {
473    /// The top level's deterministic topological emission order.
474    ///
475    /// # Errors
476    ///
477    /// Returns [`CycleDetected`] if the top-level graph contains a cycle.
478    pub fn deterministic_emit_order(&self) -> Result<Vec<BloqNodeId>, CycleDetected> {
479        self.top.deterministic_emit_order()
480    }
481}
482
483/// The panicking twin of [`Bloq::node`] (see [`SubGraph`]'s `Index` impl).
484impl Index<BloqNodeId> for Bloq {
485    type Output = BloqNode;
486
487    fn index(&self, id: BloqNodeId) -> &Self::Output {
488        &self.top[id]
489    }
490}
491
492/// The graph contains a cycle, so it has no topological emit order.
493///
494/// Named `CycleDetected` (not `CyclicGraph`) to avoid colliding with the
495/// `CyclicGraph` *variants* of [`BloqValidationError`](crate::BloqValidationError) and `ExecuteError`.
496#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
497#[error("bloq graph contains a cycle")]
498pub struct CycleDetected;
499
500/// Node-major CSR adjacency for one graph level.
501///
502/// petgraph threads each node's edges through a linked list inside the edge
503/// storage, so a Kahn walk pointer-chases ~E scattered words — at millions of
504/// edges that dominates the algorithm. Building CSR costs two sequential edge
505/// passes plus one E-sized `u32` array (and the offsets array), and the walk
506/// then reads each node's neighbors sequentially.
507struct CsrAdjacency {
508    /// `offsets[node]..offsets[node + 1]` is `node`'s slice in `targets`.
509    offsets: Vec<u32>,
510    targets: Vec<u32>,
511}
512
513impl CsrAdjacency {
514    fn of(graph: &StableDiGraph<BloqNode, BloqEdge>) -> Self {
515        let bound = graph.node_bound();
516        let mut offsets = vec![0u32; bound + 1];
517        for edge in graph.edge_references() {
518            offsets[edge.source().index() + 1] += 1;
519        }
520        for slot in 1..=bound {
521            offsets[slot] += offsets[slot - 1];
522        }
523        // Fill each node's slice from its end, decrementing the end offsets —
524        // no separate cursor array. Afterwards `offsets[i + 1]` is node `i`'s
525        // start, so rotating one slot left restores the invariant. The fill
526        // reverses the within-slice edge order, which no consumer of this
527        // structure observes.
528        let total = graph.edge_count() as u32;
529        let mut targets = vec![0u32; graph.edge_count()];
530        for edge in graph.edge_references() {
531            let from = edge.source().index();
532            offsets[from + 1] -= 1;
533            targets[offsets[from + 1] as usize] = edge.target().index() as u32;
534        }
535        offsets.remove(0);
536        offsets.push(total);
537        Self { offsets, targets }
538    }
539
540    fn slice(&self, node: usize) -> &[u32] {
541        &self.targets[self.offsets[node] as usize..self.offsets[node + 1] as usize]
542    }
543}
544
545/// The deterministic Kahn topological order of any [`BloqNode`]/[`BloqEdge`]
546/// graph (a [`Bloq`] or a region [`crate::SubGraph`] body). All edge kinds count
547/// equally toward indegree; determinism comes entirely from the min-heap keyed on
548/// node index — indices are unique, so ties never arise. Returns [`CycleDetected`]
549/// when a cycle leaves some node permanently un-emitted.
550///
551/// The walk reads a transient CSR copy of the adjacency (`CsrAdjacency`),
552/// roughly one `u32` per edge plus one per node slot, so neighbor scans are
553/// sequential instead of chasing petgraph's edge lists.
554pub(crate) fn deterministic_emit_order_of(
555    graph: &StableDiGraph<BloqNode, BloqEdge>,
556) -> Result<Vec<BloqNodeId>, CycleDetected> {
557    let adjacency = CsrAdjacency::of(graph);
558    // A node's indegree is bounded by the live edge count, which cannot
559    // approach u32::MAX within addressable memory.
560    let mut indegree = vec![0u32; graph.node_bound()];
561    for &target in &adjacency.targets {
562        indegree[target as usize] += 1;
563    }
564
565    let mut ready = BinaryHeap::new();
566    ready.extend(
567        graph
568            .node_identifiers()
569            .filter(|node| indegree[node.index()] == 0)
570            .map(|node| Reverse(node.index() as u32)),
571    );
572
573    let mut order = Vec::with_capacity(graph.node_count());
574    while let Some(Reverse(node)) = ready.pop() {
575        order.push(BloqNodeId(node));
576        // Determinism is entirely the min-heap's: node indices are unique (no
577        // ties) and each node is pushed exactly once, so push order cannot affect
578        // pop order.
579        for &target in adjacency.slice(node as usize) {
580            let target = target as usize;
581            indegree[target] -= 1;
582            if indegree[target] == 0 {
583                ready.push(Reverse(target as u32));
584            }
585        }
586    }
587
588    if order.len() != graph.node_count() {
589        return Err(CycleDetected);
590    }
591    Ok(order)
592}
593
594/// Whether the graph — plus `extra` directed edges (duplicates and parallel
595/// edges are allowed; extras naming vacant node slots are ignored) — is
596/// acyclic: the deterministic emit order's success condition, without
597/// materializing the order. Plain-stack Kahn over CSR adjacency, O(V+E) with
598/// no order or graph copy.
599pub(crate) fn is_acyclic_of(
600    graph: &StableDiGraph<BloqNode, BloqEdge>,
601    extra: &[(BloqNodeId, BloqNodeId)],
602) -> bool {
603    let adjacency = CsrAdjacency::of(graph);
604    let mut indegree = vec![0u32; graph.node_bound()];
605    for &target in &adjacency.targets {
606        indegree[target as usize] += 1;
607    }
608
609    // Extra edges grouped by source in a sorted CSR (extras are far fewer
610    // than graph edges). `source_marks` is a bitset over node slots so the
611    // per-pop probe stays O(1) for the overwhelmingly common non-source node.
612    let mut extras: Vec<(u32, u32)> = extra
613        .iter()
614        .filter(|&(from, to)| {
615            graph.contains_node(NodeIndex::new(from.0 as usize))
616                && graph.contains_node(NodeIndex::new(to.0 as usize))
617        })
618        .map(|&(from, to)| (from.0, to.0))
619        .collect();
620    extras.sort_unstable();
621    for &(_, to) in &extras {
622        indegree[to as usize] += 1;
623    }
624    let mut sources = Vec::new();
625    let mut starts = Vec::new();
626    let mut source_marks = vec![0u64; graph.node_bound().div_ceil(u64::BITS as usize)];
627    for (index, &(from, _)) in extras.iter().enumerate() {
628        if sources.last() != Some(&from) {
629            sources.push(from);
630            starts.push(index);
631            source_marks[from as usize >> 6] |= 1 << (from & 63);
632        }
633    }
634
635    let mut stack: Vec<u32> = graph
636        .node_identifiers()
637        .filter(|node| indegree[node.index()] == 0)
638        .map(|node| node.index() as u32)
639        .collect();
640    let mut visited = 0usize;
641    while let Some(node) = stack.pop() {
642        visited += 1;
643        for &target in adjacency.slice(node as usize) {
644            let target = target as usize;
645            indegree[target] -= 1;
646            if indegree[target] == 0 {
647                stack.push(target as u32);
648            }
649        }
650        if source_marks[node as usize >> 6] >> (node & 63) & 1 == 0 {
651            continue;
652        }
653        let group = sources
654            .binary_search(&node)
655            .expect("marked nodes are exactly the extra-edge sources");
656        let start = starts[group];
657        let end = starts.get(group + 1).copied().unwrap_or(extras.len());
658        for &(_, to) in &extras[start..end] {
659            let to = to as usize;
660            indegree[to] -= 1;
661            if indegree[to] == 0 {
662                stack.push(to as u32);
663            }
664        }
665    }
666
667    visited == graph.node_count()
668}
669
670#[cfg(test)]
671mod tests {
672    use bloq_circuit::{CoordCircuit, PauliBasis};
673    use glam::ivec2;
674
675    use super::*;
676    use crate::{TemplateInstance, TemplateInstanceId};
677
678    #[test]
679    fn data_inputs_exclude_activation_at_the_top_level() {
680        use crate::{ClassicalExpr, ClassicalNode};
681
682        let mut program = Bloq::new();
683        let value = program.add_node(BloqNode::classical(ClassicalNode::Compute {
684            expr: ClassicalExpr::Const(true),
685        }));
686        let activation = program.add_node(BloqNode::classical(ClassicalNode::Compute {
687            expr: ClassicalExpr::Const(false),
688        }));
689        let mut consumer = BloqNode::classical(ClassicalNode::Compute {
690            expr: ClassicalExpr::In(0),
691        });
692        consumer.activation = Some(1);
693        let consumer = program.add_node(consumer);
694        program.add_edge(value, consumer, BloqEdge::value(0));
695        program.add_edge(activation, consumer, BloqEdge::value(1));
696
697        assert_eq!(program.value_inputs(consumer).count(), 2);
698        assert_eq!(
699            program
700                .data_inputs(consumer)
701                .map(|input| (input.slot, input.producer))
702                .collect::<Vec<_>>(),
703            [(0, value)]
704        );
705    }
706
707    /// The mirror's late arrivals: both open-producer queries used to exist
708    /// only on `SubGraph`, so a caller holding a `Bloq` had to route through
709    /// `top()` for them and through `Bloq` for everything else.
710    #[test]
711    fn open_producer_queries_mirror_the_top_level() {
712        let program = Bloq::from_text(
713            "\
714BLOQIR 1
715
716template t0 {
717  circuit {
718    MPP X(0,0):m0
719  }
720}
721
722graph {
723  n0 quantum {
724    instance i0 t0 @ (0,0)
725  }
726  n1 observable fragment measurements i0:m0 from generator 0
727  n2 observable fragment operators i0 output X(0,0)
728  n0 -> n1 order
729}
730",
731        )
732        .expect("valid .bloqir text");
733
734        assert_eq!(
735            program.open_value_producers().collect::<Vec<_>>(),
736            [BloqNodeId(1), BloqNodeId(2)]
737        );
738        // Every fragment exposes its assembled parity, including empty parity.
739        assert_eq!(
740            program.open_bit_producers().collect::<Vec<_>>(),
741            [BloqNodeId(1), BloqNodeId(2)]
742        );
743    }
744
745    #[test]
746    fn graph_mutators_invalidate_layout_cache() {
747        let mut circuit = CoordCircuit::new();
748        circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
749
750        let mut bloq = Bloq::new();
751        let template = bloq.add_template(BloqTemplate::new(circuit));
752        let make_node = |instance, offset| {
753            let mut node = BloqNode::from_members(vec![]);
754            node.expect_quantum_mut()
755                .instances
756                .push(TemplateInstance::new(
757                    TemplateInstanceId(instance),
758                    template,
759                    offset,
760                ));
761            node
762        };
763        let first = bloq.add_node(make_node(0, ivec2(0, 0)));
764        let second = bloq.add_node(make_node(1, ivec2(10, 0)));
765
766        assert_eq!(
767            bloq.sorted_layout_coords().unwrap(),
768            [ivec2(0, 0), ivec2(10, 0)]
769        );
770        bloq.node_mut(first).unwrap().expect_quantum_mut().instances[0].offset = ivec2(5, 0);
771        assert_eq!(
772            bloq.sorted_layout_coords().unwrap(),
773            [ivec2(5, 0), ivec2(10, 0)]
774        );
775
776        bloq.remove_node(second).unwrap();
777        assert_eq!(bloq.sorted_layout_coords().unwrap(), [ivec2(5, 0)]);
778
779        bloq.top_mut().add_node(make_node(2, ivec2(20, 0)));
780        assert_eq!(
781            bloq.sorted_layout_coords().unwrap(),
782            [ivec2(5, 0), ivec2(20, 0)]
783        );
784    }
785
786    #[test]
787    fn declared_outputs_survive_codecs_rewrites_and_snapshot_edits() {
788        let original = Bloq::from_text(
789            "BLOQIR 1
790graph {
791 n0 compute 1
792 n1 compute !in0
793 n2 observable fragment operators
794 n0 -> n1 value 0
795 result n0
796 bindings n2
797}",
798        )
799        .unwrap();
800        original.validate().unwrap();
801        for mut program in [
802            original.clone(),
803            Bloq::from_text(&original.to_text()).unwrap(),
804            Bloq::from_binary(&original.to_binary()).unwrap(),
805            original.pin_membership(&Default::default()).unwrap(),
806        ] {
807            program.optimize().expect("acyclic test program");
808            program.validate().unwrap();
809            assert_eq!(program.value_output(), Some(BloqNodeId(0).into()));
810            assert_eq!(program.boundary_outputs(), [BloqNodeId(2)]);
811            assert!(
812                program
813                    .resolve_classical(
814                        program.value_output().unwrap().node,
815                        crate::ClassicalAssignment::Uniform(false),
816                    )
817                    .unwrap()
818                    .sign
819            );
820        }
821
822        let mut edited = original.clone();
823        edited
824            .top_mut()
825            .set_value_output(Some(BloqNodeId(1).into()));
826        edited.top_mut().set_boundary_outputs(Vec::new());
827        assert_eq!(original.value_output(), Some(BloqNodeId(0).into()));
828        assert_eq!(original.boundary_outputs(), [BloqNodeId(2)]);
829        let removed = edited.remove_node(BloqNodeId(1)).unwrap();
830        assert_eq!(edited.value_output(), None);
831        assert_eq!(edited.add_node(removed), BloqNodeId(1));
832        assert_eq!(
833            edited.value_output(),
834            None,
835            "reused ids do not retarget results"
836        );
837        edited.top_mut().set_boundary_outputs(vec![BloqNodeId(2)]);
838        let removed = edited.remove_node(BloqNodeId(2)).unwrap();
839        assert!(edited.boundary_outputs().is_empty());
840        assert_eq!(edited.add_node(removed), BloqNodeId(2));
841        assert!(edited.boundary_outputs().is_empty());
842    }
843
844    #[test]
845    fn snapshots_share_graph_levels_until_edited() {
846        use crate::{BloqNodeKind, ClassicalExpr, ClassicalNode, RegionNode, ValueRole};
847
848        let mut circuit = CoordCircuit::new();
849        circuit.measure(PauliBasis::Z, [ivec2(0, 0)]);
850        let mut original = Bloq::new();
851        let template = original.add_template(BloqTemplate::new(circuit));
852        let make_body = |instance| {
853            let mut body = SubGraph::new();
854            let mut node = BloqNode::from_members(vec![]);
855            node.expect_quantum_mut()
856                .instances
857                .push(TemplateInstance::new(
858                    TemplateInstanceId(instance),
859                    template,
860                    ivec2(0, 0),
861                ));
862            body.add_node(node);
863            body.add_node(BloqNode::classical(ClassicalNode::Compute {
864                expr: ClassicalExpr::Const(false),
865            }));
866            body
867        };
868        let selector = original.add_node(BloqNode::classical(ClassicalNode::Compute {
869            expr: ClassicalExpr::Const(false),
870        }));
871        let vacant = original.add_node(original[selector].clone());
872        let region = original.add_node(BloqNode::region(RegionNode::RepeatUntilSuccess {
873            restart_source: None,
874            restart_condition: ClassicalExpr::In(0),
875            body: make_body(0),
876        }));
877        original.remove_node(vacant).unwrap();
878        original.add_edge(
879            selector,
880            region,
881            BloqEdge::Value {
882                slot: 0,
883                role: ValueRole::Data,
884                output: crate::ObservableOutput::Corrected,
885            },
886        );
887        original.validate().unwrap();
888        let bytes = original.to_binary();
889        assert_eq!(original.sorted_layout_coords().unwrap(), [ivec2(0, 0)]);
890
891        let mut edited = original.clone();
892        assert!(std::ptr::eq(original.graph(), edited.graph()));
893        let RegionNode::RepeatUntilSuccess {
894            body: original_body,
895            ..
896        } = original[region].try_region().unwrap();
897        let BloqNodeKind::Region(RegionNode::RepeatUntilSuccess { body, .. }) =
898            &mut edited.node_mut(region).unwrap().kind
899        else {
900            panic!("region")
901        };
902        assert!(std::ptr::eq(original_body.graph(), body.graph()));
903        body.node_mut(BloqNodeId(1)).unwrap().kind = BloqNodeKind::Classical(
904            ClassicalNode::Compute {
905                expr: ClassicalExpr::Const(true),
906            }
907            .into(),
908        );
909        assert!(!std::ptr::eq(original_body.graph(), body.graph()));
910        assert!(std::ptr::eq(
911            original_body[BloqNodeId(0)].expect_quantum(),
912            body[BloqNodeId(0)].expect_quantum(),
913        ));
914        let selected = body[BloqNodeId(0)]
915            .select_quantum_members(|_| panic!("static nodes have no membership inputs"))
916            .unwrap();
917        assert!(std::ptr::eq(
918            selected.expect_quantum(),
919            body[BloqNodeId(0)].expect_quantum(),
920        ));
921        body.node_mut(BloqNodeId(0))
922            .unwrap()
923            .expect_quantum_mut()
924            .instances[0]
925            .offset = ivec2(5, 0);
926        assert!(!std::ptr::eq(
927            original_body[BloqNodeId(0)].expect_quantum(),
928            body[BloqNodeId(0)].expect_quantum(),
929        ));
930        assert_eq!(selected.expect_quantum().instances[0].offset, ivec2(0, 0));
931        assert!(!std::ptr::eq(original_body.graph(), body.graph()));
932        assert!(!std::ptr::eq(original.graph(), edited.graph()));
933        assert_eq!(original.node_ids().collect::<Vec<_>>(), [selector, region]);
934        assert_eq!(edited.node_ids().collect::<Vec<_>>(), [selector, region]);
935        assert!(edited.has_path(selector, region));
936        assert_eq!(original.sorted_layout_coords().unwrap(), [ivec2(0, 0)]);
937        assert_eq!(edited.sorted_layout_coords().unwrap(), [ivec2(5, 0)]);
938        assert_eq!(original.to_binary(), bytes);
939        assert_ne!(edited.to_binary(), bytes);
940        edited.validate().unwrap();
941        assert_eq!(Bloq::from_binary(&bytes).unwrap().to_binary(), bytes);
942    }
943
944    #[test]
945    fn layout_accessors_return_coordinate_overflow_for_unvalidated_programs() {
946        let coordinate = ivec2(i32::MAX, 0);
947        let offset = ivec2(1, 0);
948        let mut circuit = CoordCircuit::new();
949        circuit.measure(PauliBasis::Z, [coordinate]);
950        let mut bloq = Bloq::new();
951        let template = bloq.add_template(BloqTemplate::new(circuit));
952        let node = bloq.add_node({
953            let mut node = BloqNode::from_members(vec![]);
954            node.expect_quantum_mut()
955                .instances
956                .push(TemplateInstance::new(
957                    TemplateInstanceId(0),
958                    template,
959                    offset,
960                ));
961            node
962        });
963        let expected = CoordinateOverflowError { coordinate, offset };
964        let restored = Bloq::from_binary(&bloq.to_binary()).expect("binary shape decodes");
965
966        for program in [&bloq, &restored] {
967            assert_eq!(
968                program.node_qubits(program.node(node).unwrap()),
969                Err(expected)
970            );
971            assert_eq!(program.sorted_layout_coords(), Err(expected));
972            assert_eq!(program.qubit_count(), Err(expected));
973        }
974    }
975
976    // ---------------------------------------------------------------------
977    // Topological order and acyclicity
978    // ---------------------------------------------------------------------
979
980    /// The pre-optimization min-heap Kahn, kept as the correctness oracle.
981    fn heap_order(
982        graph: &StableDiGraph<BloqNode, BloqEdge>,
983    ) -> Result<Vec<BloqNodeId>, CycleDetected> {
984        let mut indegree = vec![0usize; graph.node_bound()];
985        for edge in graph.edge_references() {
986            indegree[edge.target().index()] += 1;
987        }
988        let mut ready = BinaryHeap::new();
989        ready.extend(
990            graph
991                .node_identifiers()
992                .filter(|node| indegree[node.index()] == 0)
993                .map(|node| Reverse(node.index())),
994        );
995        let mut order = Vec::with_capacity(graph.node_count());
996        while let Some(Reverse(node)) = ready.pop() {
997            order.push(BloqNodeId(node as u32));
998            for target in
999                graph.neighbors_directed(NodeIndex::new(node), petgraph::Direction::Outgoing)
1000            {
1001                let target = target.index();
1002                indegree[target] -= 1;
1003                if indegree[target] == 0 {
1004                    ready.push(Reverse(target));
1005                }
1006            }
1007        }
1008        if order.len() != graph.node_count() {
1009            return Err(CycleDetected);
1010        }
1011        Ok(order)
1012    }
1013
1014    /// A tiny deterministic LCG — `rand` is not a bloq_ir dev-dependency.
1015    fn lcg(seed: u64) -> impl FnMut() -> u64 {
1016        let mut state = seed;
1017        move || {
1018            state = state
1019                .wrapping_mul(6364136223846793005)
1020                .wrapping_add(1442695040888963407);
1021            state >> 33
1022        }
1023    }
1024
1025    fn placeholder_node() -> BloqNode {
1026        use crate::{ClassicalExpr, ClassicalNode};
1027        BloqNode::classical(ClassicalNode::Compute {
1028            expr: ClassicalExpr::Const(true),
1029        })
1030    }
1031
1032    /// Random graph with parallel edges, holes (every third node removed), and
1033    /// — when `cyclic` — a chain over the live nodes closed by a backward edge,
1034    /// so the cycle survives the removals.
1035    fn random_graph(
1036        rng: &mut impl FnMut() -> u64,
1037        nodes: usize,
1038        edges: usize,
1039        cyclic: bool,
1040    ) -> StableDiGraph<BloqNode, BloqEdge> {
1041        let mut graph = StableDiGraph::new();
1042        for _ in 0..nodes {
1043            graph.add_node(placeholder_node());
1044        }
1045        for _ in 0..edges {
1046            let from = NodeIndex::new((rng() % nodes as u64) as usize);
1047            let to = NodeIndex::new((rng() % nodes as u64) as usize);
1048            if from != to {
1049                graph.add_edge(from, to, BloqEdge::Order);
1050                if rng().is_multiple_of(4) {
1051                    graph.add_edge(from, to, BloqEdge::Order);
1052                }
1053            }
1054        }
1055        for slot in (0..nodes).step_by(3) {
1056            graph.remove_node(NodeIndex::new(slot));
1057        }
1058        let live: Vec<NodeIndex> = graph.node_identifiers().collect();
1059        if cyclic && live.len() > 1 {
1060            for pair in live.windows(2) {
1061                graph.add_edge(pair[0], pair[1], BloqEdge::Order);
1062            }
1063            let (first, last) = (live[0], live[live.len() - 1]);
1064            graph.add_edge(last, first, BloqEdge::Order);
1065        }
1066        graph
1067    }
1068
1069    #[test]
1070    fn emit_order_and_acyclicity_match_the_heap_reference() {
1071        let mut rng = lcg(0x5EED_1234);
1072        for case in 0..60 {
1073            let nodes = 1 + (rng() % 40) as usize;
1074            let edges = (rng() % 80) as usize;
1075            let cyclic = rng().is_multiple_of(3);
1076            let graph = random_graph(&mut rng, nodes, edges, cyclic);
1077            assert_eq!(
1078                deterministic_emit_order_of(&graph),
1079                heap_order(&graph),
1080                "case {case}"
1081            );
1082            assert_eq!(
1083                is_acyclic_of(&graph, &[]),
1084                deterministic_emit_order_of(&graph).is_ok(),
1085                "case {case}"
1086            );
1087        }
1088    }
1089
1090    #[test]
1091    fn is_acyclic_with_edges_matches_a_copied_graph_oracle() {
1092        let mut rng = lcg(0x0BAD_C0DE);
1093        for case in 0..60 {
1094            let nodes = 1 + (rng() % 30) as usize;
1095            let edges = (rng() % 60) as usize;
1096            let cyclic = rng().is_multiple_of(2);
1097            let graph = random_graph(&mut rng, nodes, edges, cyclic);
1098            let live: Vec<u32> = graph
1099                .node_identifiers()
1100                .map(|node| node.index() as u32)
1101                .collect();
1102            let extra: Vec<(BloqNodeId, BloqNodeId)> = if live.is_empty() {
1103                Vec::new()
1104            } else {
1105                (0..rng() % 8)
1106                    .map(|_| {
1107                        let from = live[(rng() % live.len() as u64) as usize];
1108                        let to = live[(rng() % live.len() as u64) as usize];
1109                        (BloqNodeId(from), BloqNodeId(to))
1110                    })
1111                    .collect()
1112            };
1113            let mut copy = petgraph::graph::DiGraph::<(), ()>::new();
1114            let slots: Vec<_> = (0..graph.node_bound()).map(|_| copy.add_node(())).collect();
1115            for edge in graph.edge_references() {
1116                copy.add_edge(
1117                    slots[edge.source().index()],
1118                    slots[edge.target().index()],
1119                    (),
1120                );
1121            }
1122            for &(from, to) in &extra {
1123                copy.add_edge(slots[from.0 as usize], slots[to.0 as usize], ());
1124            }
1125            assert_eq!(
1126                is_acyclic_of(&graph, &extra),
1127                !petgraph::algo::is_cyclic_directed(&copy),
1128                "case {case}, extra {extra:?}"
1129            );
1130        }
1131    }
1132
1133    #[test]
1134    fn is_acyclic_with_edges_detects_single_and_combined_extra_cycles() {
1135        // 0 -> 1 -> 2, node 3 isolated.
1136        let mut graph = StableDiGraph::new();
1137        for _ in 0..4 {
1138            graph.add_node(placeholder_node());
1139        }
1140        graph.add_edge(NodeIndex::new(0), NodeIndex::new(1), BloqEdge::Order);
1141        graph.add_edge(NodeIndex::new(1), NodeIndex::new(2), BloqEdge::Order);
1142        let id = |slot: u32| BloqNodeId(slot);
1143
1144        assert!(is_acyclic_of(&graph, &[]));
1145        assert!(is_acyclic_of(&graph, &[(id(2), id(3))]));
1146        assert!(is_acyclic_of(&graph, &[(id(0), id(2)), (id(0), id(2))]));
1147        assert!(!is_acyclic_of(&graph, &[(id(2), id(0))]));
1148        assert!(!is_acyclic_of(&graph, &[(id(1), id(1))]));
1149        // Neither extra closes a cycle alone; together they do.
1150        assert!(is_acyclic_of(&graph, &[(id(2), id(3))]));
1151        assert!(is_acyclic_of(&graph, &[(id(3), id(0))]));
1152        assert!(!is_acyclic_of(&graph, &[(id(2), id(3)), (id(3), id(0))]));
1153        // Extra edges with stale endpoints name no node and are ignored.
1154        assert_eq!(
1155            is_acyclic_of(&graph, &[(id(u32::MAX), id(0))]),
1156            is_acyclic_of(&graph, &[])
1157        );
1158    }
1159}