Skip to main content

bloq_ir/ir/
graph.rs

1use std::fmt;
2use std::ops::Index;
3use std::sync::Arc;
4
5use petgraph::Direction;
6use petgraph::stable_graph::{NodeIndex, StableDiGraph};
7use petgraph::visit::{EdgeRef, IntoEdgeReferences, Visitable};
8
9use super::{BloqEdge, BloqNode, BloqNodeId, ClassicalExpr, ClassicalNode, QuantumNode, ValueRole};
10
11mod classical_data;
12
13/// A nested region body.
14///
15/// This graph uses the **same** global [`crate::BloqTemplatePool`] as the
16/// enclosing [`Bloq`](crate::Bloq) — template ids resolve against
17/// that shared pool, so a `SubGraph` carries none of its own. Edges do not cross
18/// region boundaries. The body's declared result and boundary bindings form
19/// its classical interface (WF-17/SEM-RVAL).
20/// Clones share immutable storage. Mutable access copies only this graph level
21/// when shared; nested bodies remain shared until they too are edited.
22#[derive(Debug, Clone, Default)]
23pub struct SubGraph {
24    data: Arc<SubGraphData>,
25}
26
27#[derive(Debug, Clone, Default)]
28struct SubGraphData {
29    graph: StableDiGraph<BloqNode, BloqEdge>,
30    value_output: Option<super::ValueRef>,
31    boundary_outputs: Vec<BloqNodeId>,
32}
33
34/// Why a serialized graph level could not be rebuilt ([`rebuild_level`]).
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub(crate) enum LevelRebuildError {
37    DuplicateNode(u32),
38    /// The `index`-th edge (in listing order) names a vacant/out-of-range id.
39    MissingEndpoint {
40        index: usize,
41        id: u32,
42    },
43    /// The id space is far sparser than any node-removing pass produces —
44    /// rebuilding would materialize `holes` placeholder slots (cost is
45    /// O(largest id), not O(node count)), so a crafted artifact with one huge
46    /// id must be rejected here, before [`crate::Bloq::validate`] can run.
47    ExcessiveVacancy {
48        holes: usize,
49        live: usize,
50    },
51}
52
53impl fmt::Display for LevelRebuildError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Self::DuplicateNode(id) => write!(f, "duplicate node n{id}"),
57            Self::MissingEndpoint { id, .. } => {
58                write!(f, "edge endpoint n{id} is not a live node")
59            }
60            Self::ExcessiveVacancy { holes, live } => write!(
61                f,
62                "node ids are implausibly sparse: {holes} vacant slots for {live} live nodes"
63            ),
64        }
65    }
66}
67
68/// Decode-time cap on vacant node slots per level, over and above a 4× live
69/// multiple. Holes only ever come from node-removing passes, which delete at
70/// most a few times the surviving node count; this bound is far past that
71/// while keeping placeholder materialization cheap.
72const MAX_EXTRA_VACANCIES: usize = 1 << 16;
73
74/// Rebuild one graph level from `(id, node)` pairs and `(source, target,
75/// edge)` triples — the shared materialization step of both exchange formats.
76///
77/// Node ids are honored exactly: ids missing below the last live one become
78/// vacant slots (a placeholder node is added at the slot, then removed).
79/// Edges are added in the given order, so live-edge iteration order survives
80/// a round trip. Vacancy past the last live node and the free-slot reuse
81/// order are *not* representable — non-semantic history that only affects
82/// the ids future `add_node`/`add_edge` calls hand out.
83///
84/// Vacancy is capped ([`LevelRebuildError::ExcessiveVacancy`]): rebuilding
85/// costs O(largest id), so an untrusted artifact must not be able to demand
86/// billions of placeholder slots with a single huge id.
87pub(crate) fn rebuild_level(
88    mut nodes: Vec<(u32, BloqNode)>,
89    edges: Vec<(u32, u32, BloqEdge)>,
90) -> Result<SubGraph, LevelRebuildError> {
91    nodes.sort_by_key(|(id, _)| *id);
92    if let Some(window) = nodes.windows(2).find(|window| window[0].0 == window[1].0) {
93        return Err(LevelRebuildError::DuplicateNode(window[0].0));
94    }
95    let bound = nodes.last().map_or(0, |(id, _)| u64::from(*id) + 1);
96    let live = nodes.len();
97    let holes = bound - live as u64;
98    let vacancy_error = || LevelRebuildError::ExcessiveVacancy {
99        holes: usize::try_from(holes).unwrap_or(usize::MAX),
100        live,
101    };
102    if holes
103        > (live as u64)
104            .saturating_mul(4)
105            .saturating_add(MAX_EXTRA_VACANCIES as u64)
106    {
107        return Err(vacancy_error());
108    }
109    let bound = usize::try_from(bound).map_err(|_| vacancy_error())?;
110
111    let mut level = SubGraph::new();
112    level.graph_mut().reserve_exact_nodes(bound);
113    let mut pending = nodes.into_iter().peekable();
114    let mut placeholders = Vec::new();
115    for slot in 0..bound {
116        if pending.peek().is_some_and(|(id, _)| *id as usize == slot) {
117            let (_, node) = pending.next().expect("peek said so");
118            level.add_node(node);
119        } else {
120            placeholders.push(level.add_node(BloqNode::classical(ClassicalNode::Compute {
121                expr: ClassicalExpr::Const(false),
122            })));
123        }
124    }
125    drop(pending);
126    for placeholder in placeholders {
127        level
128            .graph_mut()
129            .remove_node(to_index(placeholder))
130            .expect("placeholder was just added");
131    }
132
133    for (index, (source, target, _)) in edges.iter().enumerate() {
134        check_edge_endpoints(&level, index, *source, *target)?;
135    }
136    level.graph_mut().reserve_exact_edges(edges.len());
137    for (source, target, edge) in edges {
138        level.add_edge(BloqNodeId(source), BloqNodeId(target), edge);
139    }
140    Ok(level)
141}
142
143fn check_edge_endpoints(
144    level: &SubGraph,
145    index: usize,
146    source: u32,
147    target: u32,
148) -> Result<(), LevelRebuildError> {
149    for id in [source, target] {
150        if level.node(BloqNodeId(id)).is_none() {
151            return Err(LevelRebuildError::MissingEndpoint { index, id });
152        }
153    }
154    Ok(())
155}
156
157/// Canonical wire shape for a graph level: live nodes with explicit slot ids,
158/// live edges in slot order. Deliberately independent of petgraph's own serde
159/// layout (which also encodes vacant *edge* slots — non-semantic free-list
160/// state that would make equal programs serialize unequally).
161#[derive(serde::Serialize)]
162struct SubGraphWireRef<'a> {
163    #[serde(serialize_with = "serialize_nodes")]
164    nodes: &'a SubGraph,
165    #[serde(serialize_with = "serialize_edges")]
166    edges: &'a SubGraph,
167    value_output: Option<super::ValueRef>,
168    boundary_outputs: &'a [BloqNodeId],
169}
170
171// Stream the known live counts, preserving slot order without temporary
172// reference arrays. Iterator size hints include holes and are not exact.
173fn serialize_nodes<S: serde::Serializer>(
174    graph: &SubGraph,
175    serializer: S,
176) -> Result<S::Ok, S::Error> {
177    use serde::ser::SerializeSeq;
178    let mut sequence = serializer.serialize_seq(Some(graph.node_count()))?;
179    for (id, node) in graph.nodes() {
180        sequence.serialize_element(&(id.0, node))?;
181    }
182    sequence.end()
183}
184
185fn serialize_edges<S: serde::Serializer>(
186    graph: &SubGraph,
187    serializer: S,
188) -> Result<S::Ok, S::Error> {
189    use serde::ser::SerializeSeq;
190    if !serializer.is_human_readable() {
191        return edge_runs::serialize(graph, serializer);
192    }
193    let mut sequence = serializer.serialize_seq(Some(graph.edge_count()))?;
194    for edge in graph.edges() {
195        sequence.serialize_element(&(edge.source.0, edge.target.0, edge.edge))?;
196    }
197    sequence.end()
198}
199
200#[derive(serde::Deserialize)]
201struct SubGraphWire {
202    nodes: Vec<(u32, BloqNode)>,
203    edges: Vec<(u32, u32, BloqEdge)>,
204    value_output: Option<super::ValueRef>,
205    boundary_outputs: Vec<BloqNodeId>,
206}
207
208/// Binary-only edge packing. The in-memory graph and human-readable triples
209/// retain every edge; runs only omit repeated target, role and consecutive slots.
210mod edge_runs {
211    use super::{BloqEdge, BloqNodeId, SubGraph, ValueRole, check_edge_endpoints};
212    use crate::{ObservableOutput, QuantumEdge};
213    use serde::de::{DeserializeSeed, EnumAccess, Error, SeqAccess, VariantAccess, Visitor};
214    use serde::ser::SerializeSeq;
215
216    #[derive(serde::Serialize)]
217    enum Edge<'a> {
218        Quantum(u32, u32, &'a QuantumEdge),
219        Value(u32, u32, u32, &'a ValueRole, ObservableOutput),
220        Compose(u32, u32, u32, &'a ValueRole),
221        Order(u32, u32),
222        ValueRun(u32, u32, &'a ValueRole, ObservableOutput, &'a [u32]),
223    }
224
225    #[derive(serde::Deserialize)]
226    #[serde(field_identifier)]
227    enum EdgeKind {
228        Quantum,
229        Value,
230        Compose,
231        Order,
232        ValueRun,
233    }
234
235    pub(super) fn serialize<S: serde::Serializer>(
236        graph: &SubGraph,
237        serializer: S,
238    ) -> Result<S::Ok, S::Error> {
239        let mut count = 0usize;
240        let mut previous: Option<(crate::BloqNodeId, u32, &ValueRole, ObservableOutput)> = None;
241        for edge in graph.edges() {
242            let next = match edge.edge {
243                BloqEdge::Value { slot, role, output } => Some((edge.target, *slot, role, *output)),
244                _ => None,
245            };
246            let continues = match (previous, next) {
247                (
248                    Some((target, slot, role, output)),
249                    Some((next_target, next_slot, next_role, next_output)),
250                ) => {
251                    target == next_target
252                        && role == next_role
253                        && output == next_output
254                        && slot.checked_add(1) == Some(next_slot)
255                }
256                _ => false,
257            };
258            count += usize::from(!continues);
259            previous = next;
260        }
261        let mut sequence = serializer.serialize_seq(Some(count))?;
262        let mut edges = graph.edges().peekable();
263        // One reusable run buffer, bounded by the largest consecutive fan-in.
264        let mut sources = Vec::new();
265        while let Some(edge) = edges.next() {
266            let encoded = match edge.edge {
267                BloqEdge::Quantum(value) => Edge::Quantum(edge.source.0, edge.target.0, value),
268                BloqEdge::Order => Edge::Order(edge.source.0, edge.target.0),
269                BloqEdge::Compose { slot, role } => {
270                    Edge::Compose(edge.source.0, edge.target.0, *slot, role)
271                }
272                BloqEdge::Value { slot, role, output } => {
273                    sources.clear();
274                    sources.push(edge.source.0);
275                    while let Some(next) = edges.peek() {
276                        let BloqEdge::Value {
277                            slot: next_slot,
278                            role: next_role,
279                            output: next_output,
280                        } = next.edge
281                        else {
282                            break;
283                        };
284                        if next.target != edge.target
285                            || next_role != role
286                            || next_output != output
287                            || u32::try_from(sources.len())
288                                .ok()
289                                .and_then(|length| slot.checked_add(length))
290                                != Some(*next_slot)
291                        {
292                            break;
293                        }
294                        sources.push(edges.next().expect("peeked edge").source.0);
295                    }
296                    if sources.len() == 1 {
297                        Edge::Value(edge.source.0, edge.target.0, *slot, role, *output)
298                    } else {
299                        Edge::ValueRun(edge.target.0, *slot, role, *output, &sources)
300                    }
301                }
302            };
303            sequence.serialize_element(&encoded)?;
304        }
305        sequence.end()
306    }
307
308    fn add_edge<E: Error>(
309        level: &mut SubGraph,
310        source: u32,
311        target: u32,
312        edge: BloqEdge,
313    ) -> Result<(), E> {
314        check_edge_endpoints(level, level.edge_count(), source, target).map_err(E::custom)?;
315        level.add_edge(BloqNodeId(source), BloqNodeId(target), edge);
316        Ok(())
317    }
318
319    pub(super) struct EdgeSequence<'a>(pub(super) &'a mut SubGraph);
320    impl<'de> DeserializeSeed<'de> for EdgeSequence<'_> {
321        type Value = ();
322        fn deserialize<D: serde::Deserializer<'de>>(self, deserializer: D) -> Result<(), D::Error> {
323            deserializer.deserialize_seq(self)
324        }
325    }
326    impl<'de> Visitor<'de> for EdgeSequence<'_> {
327        type Value = ();
328        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329            formatter.write_str("graph edges and consecutive value-edge runs")
330        }
331        fn visit_seq<A: SeqAccess<'de>>(self, mut sequence: A) -> Result<(), A::Error> {
332            // An untrusted count is not permission to allocate the whole graph.
333            let capacity = sequence
334                .size_hint()
335                .unwrap_or(0)
336                .min((1 << 20) / size_of::<petgraph::graph::Edge<Option<BloqEdge>>>());
337            self.0.graph_mut().reserve_exact_edges(capacity);
338            while sequence.next_element_seed(EdgeSeed(self.0))?.is_some() {}
339            self.0.graph_mut().shrink_to_fit_edges();
340            Ok(())
341        }
342    }
343
344    struct EdgeSeed<'a>(&'a mut SubGraph);
345    impl<'de> DeserializeSeed<'de> for EdgeSeed<'_> {
346        type Value = ();
347        fn deserialize<D: serde::Deserializer<'de>>(self, deserializer: D) -> Result<(), D::Error> {
348            deserializer.deserialize_enum(
349                "Edge",
350                &["Quantum", "Value", "Compose", "Order", "ValueRun"],
351                self,
352            )
353        }
354    }
355    impl<'de> Visitor<'de> for EdgeSeed<'_> {
356        type Value = ();
357        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358            formatter.write_str("one graph edge or value-edge run")
359        }
360        fn visit_enum<A: EnumAccess<'de>>(self, data: A) -> Result<(), A::Error> {
361            let (kind, value) = data.variant::<EdgeKind>()?;
362            match kind {
363                EdgeKind::Quantum => {
364                    let (source, target, edge) =
365                        value.newtype_variant::<(u32, u32, QuantumEdge)>()?;
366                    add_edge::<A::Error>(
367                        self.0,
368                        source,
369                        target,
370                        BloqEdge::Quantum(Box::new(edge)),
371                    )?;
372                }
373                EdgeKind::Value => {
374                    let (source, target, slot, role, output) =
375                        value.newtype_variant::<(u32, u32, u32, ValueRole, ObservableOutput)>()?;
376                    add_edge::<A::Error>(
377                        self.0,
378                        source,
379                        target,
380                        BloqEdge::Value { slot, role, output },
381                    )?;
382                }
383                EdgeKind::Compose => {
384                    let (source, target, slot, role) =
385                        value.newtype_variant::<(u32, u32, u32, ValueRole)>()?;
386                    add_edge::<A::Error>(self.0, source, target, BloqEdge::Compose { slot, role })?;
387                }
388                EdgeKind::Order => {
389                    let (source, target) = value.newtype_variant::<(u32, u32)>()?;
390                    add_edge::<A::Error>(self.0, source, target, BloqEdge::Order)?;
391                }
392                EdgeKind::ValueRun => return value.tuple_variant(5, Run(self.0)),
393            }
394            Ok(())
395        }
396    }
397
398    struct Run<'a>(&'a mut SubGraph);
399    impl<'de> Visitor<'de> for Run<'_> {
400        type Value = ();
401        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402            formatter.write_str("target, first slot, role, output and source IDs")
403        }
404        fn visit_seq<A: SeqAccess<'de>>(self, mut sequence: A) -> Result<(), A::Error> {
405            let target = sequence
406                .next_element()?
407                .ok_or_else(|| A::Error::custom("value run needs a target"))?;
408            let slot = sequence
409                .next_element()?
410                .ok_or_else(|| A::Error::custom("value run needs a slot"))?;
411            let role = sequence
412                .next_element()?
413                .ok_or_else(|| A::Error::custom("value run needs a role"))?;
414            let output = sequence
415                .next_element()?
416                .ok_or_else(|| A::Error::custom("value run needs an output"))?;
417            sequence
418                .next_element_seed(Sources {
419                    edges: self.0,
420                    target,
421                    slot,
422                    role,
423                    output,
424                })?
425                .ok_or_else(|| A::Error::custom("value run needs source IDs"))
426        }
427    }
428
429    struct Sources<'a> {
430        edges: &'a mut SubGraph,
431        target: u32,
432        slot: u32,
433        role: ValueRole,
434        output: ObservableOutput,
435    }
436    impl<'de> DeserializeSeed<'de> for Sources<'_> {
437        type Value = ();
438        fn deserialize<D: serde::Deserializer<'de>>(self, deserializer: D) -> Result<(), D::Error> {
439            deserializer.deserialize_seq(self)
440        }
441    }
442    impl<'de> Visitor<'de> for Sources<'_> {
443        type Value = ();
444        fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445            formatter.write_str("at least two source IDs with consecutive slots")
446        }
447        fn visit_seq<A: SeqAccess<'de>>(self, mut sequence: A) -> Result<(), A::Error> {
448            let mut count = 0usize;
449            while let Some(source) = sequence.next_element::<u32>()? {
450                let offset = u32::try_from(count)
451                    .map_err(|_| A::Error::custom("value run slot overflow"))?;
452                let slot = self
453                    .slot
454                    .checked_add(offset)
455                    .ok_or_else(|| A::Error::custom("value run slot overflow"))?;
456                // Expand directly into graph storage, without an intermediate
457                // edge or source vector.
458                add_edge::<A::Error>(
459                    self.edges,
460                    source,
461                    self.target,
462                    BloqEdge::Value {
463                        slot,
464                        role: self.role.clone(),
465                        output: self.output,
466                    },
467                )?;
468                count += 1;
469            }
470            if count < 2 {
471                return Err(A::Error::custom("value run needs at least two sources"));
472            }
473            Ok(())
474        }
475    }
476
477    #[cfg(test)]
478    mod tests {
479        use super::*;
480        use crate::{BloqNode, ClassicalExpr, ClassicalNode, NodeProvenance, ValueRef};
481
482        #[derive(Clone, serde::Serialize)]
483        enum WireKind {
484            // Keep the same variant index as the binary node-kind wire.
485            #[allow(
486                dead_code,
487                reason = "the wire tag must match the production quantum variant"
488            )]
489            Quantum,
490            Classical(u32),
491        }
492
493        #[derive(Clone, serde::Serialize)]
494        struct WireNode {
495            kind: WireKind,
496            provenance: NodeProvenance,
497            activation: Option<u32>,
498        }
499
500        struct Decoded(SubGraph);
501
502        impl<'de> serde::Deserialize<'de> for Decoded {
503            fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
504                let mut graph = SubGraph::new();
505                for _ in 0..10 {
506                    graph.add_node(BloqNode::classical(ClassicalNode::Compute {
507                        expr: ClassicalExpr::Const(false),
508                    }));
509                }
510                EdgeSequence(&mut graph).deserialize(deserializer)?;
511                Ok(Self(graph))
512            }
513        }
514
515        fn encode(value: &impl serde::Serialize) -> Vec<u8> {
516            postcard::to_extend(value, Vec::new()).unwrap()
517        }
518
519        #[test]
520        fn value_runs_preserve_full_width_slots_duplicates_roles_and_edge_order() {
521            let role = ValueRole::FeedbackFold { action: u32::MAX };
522            let source = [9, 9];
523            let encoded = encode(&vec![
524                Edge::Value(1, 2, 7, &ValueRole::Data, ObservableOutput::Corrected),
525                Edge::Order(2, 3),
526                Edge::ValueRun(9, u32::MAX - 1, &role, ObservableOutput::Flip, &source),
527                Edge::Value(
528                    3,
529                    9,
530                    0,
531                    &ValueRole::ReadoutFold,
532                    ObservableOutput::Corrected,
533                ),
534            ]);
535            let expected = vec![
536                (1, 2, BloqEdge::value(7)),
537                (2, 3, BloqEdge::Order),
538                (
539                    9,
540                    9,
541                    BloqEdge::Value {
542                        slot: u32::MAX - 1,
543                        role: role.clone(),
544                        output: ObservableOutput::Flip,
545                    },
546                ),
547                (
548                    9,
549                    9,
550                    BloqEdge::Value {
551                        slot: u32::MAX,
552                        role,
553                        output: ObservableOutput::Flip,
554                    },
555                ),
556                (
557                    3,
558                    9,
559                    BloqEdge::Value {
560                        slot: 0,
561                        role: ValueRole::ReadoutFold,
562                        output: ObservableOutput::Corrected,
563                    },
564                ),
565            ];
566            assert_eq!(
567                postcard::from_bytes::<Decoded>(&encoded)
568                    .unwrap()
569                    .0
570                    .edges()
571                    .map(|edge| (edge.source.0, edge.target.0, edge.edge.clone()))
572                    .collect::<Vec<_>>(),
573                expected
574            );
575            for end in 0..encoded.len() {
576                assert!(postcard::from_bytes::<Decoded>(&encoded[..end]).is_err());
577            }
578        }
579
580        #[test]
581        fn value_run_decoder_rejects_invalid_lengths_slots_and_endpoints() {
582            for (slot, sources) in [(0, vec![]), (0, vec![0]), (u32::MAX, vec![0, 1])] {
583                let encoded = encode(&vec![Edge::ValueRun(
584                    0,
585                    slot,
586                    &ValueRole::Data,
587                    ObservableOutput::Flip,
588                    &sources,
589                )]);
590                assert!(postcard::from_bytes::<Decoded>(&encoded).is_err());
591            }
592            // Declared counts without payload must not reserve those counts.
593            for encoded in [
594                encode(&usize::MAX),
595                encode(&(
596                    1usize,
597                    4u32,
598                    0u32,
599                    0u32,
600                    ValueRole::Data,
601                    ObservableOutput::Flip,
602                    usize::MAX,
603                )),
604                encode(&(1usize, 5u32)),
605            ] {
606                assert!(postcard::from_bytes::<Decoded>(&encoded).is_err());
607            }
608            #[derive(serde::Serialize)]
609            struct InvalidLevel<'a> {
610                classical: Vec<ClassicalNode>,
611                nodes: Vec<(u32, WireNode)>,
612                edges: Vec<Edge<'a>>,
613                value_output: Option<ValueRef>,
614                boundary_outputs: Vec<BloqNodeId>,
615            }
616            let mut wire = InvalidLevel {
617                classical: vec![ClassicalNode::Compute {
618                    expr: ClassicalExpr::Const(false),
619                }],
620                nodes: vec![(
621                    0,
622                    WireNode {
623                        kind: WireKind::Classical(0),
624                        provenance: NodeProvenance::None,
625                        activation: None,
626                    },
627                )],
628                edges: vec![Edge::ValueRun(
629                    0,
630                    0,
631                    &ValueRole::Data,
632                    ObservableOutput::Flip,
633                    &[0, 1],
634                )],
635                value_output: None,
636                boundary_outputs: Vec::new(),
637            };
638            postcard::from_bytes::<SubGraph>(&encode(&wire)).unwrap_err();
639            wire.edges.clear();
640            postcard::from_bytes::<SubGraph>(&encode(&wire)).unwrap();
641            wire.nodes.push(wire.nodes[0].clone());
642            postcard::from_bytes::<SubGraph>(&encode(&wire)).unwrap_err();
643            wire.nodes.pop();
644            wire.nodes[0].0 = u32::MAX;
645            postcard::from_bytes::<SubGraph>(&encode(&wire)).unwrap_err();
646        }
647
648        #[test]
649        fn binary_graph_accepts_unsorted_nodes_and_checks_every_edge_endpoint() {
650            let definitions = vec![ClassicalNode::Compute {
651                expr: ClassicalExpr::Const(false),
652            }];
653            let node = WireNode {
654                kind: WireKind::Classical(0),
655                provenance: NodeProvenance::None,
656                activation: None,
657            };
658            let nodes = vec![(2u32, node.clone()), (0u32, node)];
659            let wire = (
660                &definitions,
661                &nodes,
662                vec![Edge::ValueRun(
663                    2,
664                    0,
665                    &ValueRole::Data,
666                    ObservableOutput::Flip,
667                    &[0, 0],
668                )],
669                Some(ValueRef::from(BloqNodeId(2))),
670                vec![BloqNodeId(0)],
671            );
672            let bytes = encode(&wire);
673            let mut restored = postcard::from_bytes::<SubGraph>(&bytes).unwrap();
674            assert!(restored.node(BloqNodeId(1)).is_none());
675            assert_eq!(restored.value_output(), Some(ValueRef::from(BloqNodeId(2))));
676            assert_eq!(restored.boundary_outputs(), &[BloqNodeId(0)]);
677            let mut reloaded = postcard::from_bytes::<SubGraph>(&encode(&restored)).unwrap();
678            for level in [&mut restored, &mut reloaded] {
679                level.add_edge(BloqNodeId(2), BloqNodeId(0), BloqEdge::Order);
680            }
681            assert_eq!(encode(&restored), encode(&reloaded));
682            for end in 0..bytes.len() {
683                postcard::from_bytes::<SubGraph>(&bytes[..end]).unwrap_err();
684            }
685
686            let quantum = QuantumEdge {
687                pipes: Vec::new(),
688                guard: None,
689            };
690            for edge in [
691                Edge::Quantum(1, 2, &quantum),
692                Edge::Value(0, 1, 0, &ValueRole::Data, ObservableOutput::Corrected),
693                Edge::Order(1, 2),
694                Edge::Compose(0, 1, 0, &ValueRole::Data),
695                Edge::ValueRun(2, 0, &ValueRole::Data, ObservableOutput::Flip, &[0, 1]),
696            ] {
697                let invalid = encode(&(
698                    &definitions,
699                    &nodes,
700                    vec![edge],
701                    None::<ValueRef>,
702                    Vec::<BloqNodeId>::new(),
703                ));
704                postcard::from_bytes::<SubGraph>(&invalid).unwrap_err();
705            }
706        }
707
708        #[test]
709        fn graph_runs_keep_node_holes_declarations_and_human_readable_triples() {
710            let mut graph = SubGraph::new();
711            let nodes = (0..6)
712                .map(|_| {
713                    graph.add_node(BloqNode::classical(ClassicalNode::Compute {
714                        expr: ClassicalExpr::Const(false),
715                    }))
716                })
717                .collect::<Vec<_>>();
718            graph.remove_node(nodes[1]).unwrap();
719            for (source, slot, role) in [
720                (0, 0, ValueRole::Data),
721                (0, 1, ValueRole::Data),
722                (2, 2, ValueRole::FeedbackFold { action: 9 }),
723                (3, 3, ValueRole::FeedbackFold { action: 9 }),
724                (2, 4, ValueRole::ReadoutFold),
725                (2, 5, ValueRole::ReadoutFold),
726                (3, u32::MAX, ValueRole::Data),
727            ] {
728                graph.add_edge(
729                    nodes[source],
730                    nodes[4],
731                    BloqEdge::Value {
732                        slot,
733                        role,
734                        output: if slot % 2 == 0 {
735                            ObservableOutput::Flip
736                        } else {
737                            ObservableOutput::Corrected
738                        },
739                    },
740                );
741                if slot == 1 {
742                    graph.add_edge(nodes[2], nodes[5], BloqEdge::Order);
743                }
744            }
745            graph.add_edge(nodes[0], nodes[4], BloqEdge::compose(100));
746            graph.node_mut(nodes[4]).unwrap().kind = crate::BloqNodeKind::Classical(
747                ClassicalNode::Compute {
748                    expr: ClassicalExpr::parity([0, 1, 2, 3, 4, 5, u32::MAX], false),
749                }
750                .into(),
751            );
752            graph.set_value_output(Some(ValueRef::from(nodes[4])));
753            let json = serde_json::to_value(&graph).unwrap();
754            let triples = graph
755                .edges()
756                .map(|edge| (edge.source.0, edge.target.0, edge.edge))
757                .collect::<Vec<_>>();
758            assert_eq!(json["edges"], serde_json::to_value(triples).unwrap());
759            let binary = encode(&graph);
760            let mut unsorted_json = json.clone();
761            unsorted_json["nodes"].as_array_mut().unwrap().reverse();
762            for restored in [
763                postcard::from_bytes::<SubGraph>(&binary).unwrap(),
764                serde_json::from_value::<SubGraph>(json.clone()).unwrap(),
765                serde_json::from_value::<SubGraph>(unsorted_json).unwrap(),
766            ] {
767                assert!(restored.node(nodes[1]).is_none());
768                assert_eq!(restored.value_output(), Some(ValueRef::from(nodes[4])));
769                assert_eq!(serde_json::to_value(&restored).unwrap(), json);
770                assert_eq!(encode(&restored), binary);
771            }
772        }
773    }
774}
775
776impl serde::Serialize for SubGraph {
777    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
778        if !serializer.is_human_readable() {
779            return classical_data::serialize(self, serializer);
780        }
781        SubGraphWireRef {
782            nodes: self,
783            edges: self,
784            value_output: self.value_output(),
785            boundary_outputs: self.boundary_outputs(),
786        }
787        .serialize(serializer)
788    }
789}
790
791impl<'de> serde::Deserialize<'de> for SubGraph {
792    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
793        if !deserializer.is_human_readable() {
794            return deserializer.deserialize_struct(
795                "SubGraphWire",
796                &[
797                    "classical",
798                    "nodes",
799                    "edges",
800                    "value_output",
801                    "boundary_outputs",
802                ],
803                BinaryGraph,
804            );
805        }
806        let wire = SubGraphWire::deserialize(deserializer)?;
807        let mut level = rebuild_level(wire.nodes, wire.edges).map_err(serde::de::Error::custom)?;
808        level.set_value_output(wire.value_output);
809        level.set_boundary_outputs(wire.boundary_outputs);
810        level.share_local_classical_data();
811        Ok(level)
812    }
813}
814
815struct BinaryGraph;
816
817impl<'de> serde::de::Visitor<'de> for BinaryGraph {
818    type Value = SubGraph;
819
820    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
821        formatter.write_str("graph nodes, edges and output declarations")
822    }
823
824    fn visit_seq<A: serde::de::SeqAccess<'de>>(
825        self,
826        mut sequence: A,
827    ) -> Result<SubGraph, A::Error> {
828        use serde::de::Error;
829
830        let nodes = classical_data::read_nodes(&mut sequence)?;
831        // Reuse the shared node-id and vacancy checks, then release the staged
832        // node buffer before allocating any decoded edges.
833        let mut level = rebuild_level(nodes, Vec::new()).map_err(A::Error::custom)?;
834        sequence
835            .next_element_seed(edge_runs::EdgeSequence(&mut level))?
836            .ok_or_else(|| A::Error::missing_field("edges"))?;
837        level.set_value_output(
838            sequence
839                .next_element()?
840                .ok_or_else(|| A::Error::missing_field("value_output"))?,
841        );
842        level.set_boundary_outputs(
843            sequence
844                .next_element()?
845                .ok_or_else(|| A::Error::missing_field("boundary_outputs"))?,
846        );
847        Ok(level)
848    }
849}
850
851/// A borrowed graph edge with id-space endpoints — the id-native view of one
852/// [`BloqEdge`], so traversal never exposes petgraph types.
853#[derive(Debug, Clone, Copy)]
854pub struct BloqEdgeRef<'a> {
855    /// Source node.
856    pub source: BloqNodeId,
857    /// Target node.
858    pub target: BloqNodeId,
859    /// Borrowed edge payload.
860    pub edge: &'a BloqEdge,
861}
862
863/// Reusable visit-map scratch for batched reachability probes
864/// ([`SubGraph::has_path_with_scratch`]). Opaque so petgraph stays sealed
865/// behind the IR.
866#[doc(hidden)]
867#[derive(Debug)]
868pub struct PathScratch(
869    petgraph::algo::DfsSpace<NodeIndex, <StableDiGraph<BloqNode, BloqEdge> as Visitable>::Map>,
870);
871
872fn to_index(id: BloqNodeId) -> NodeIndex {
873    NodeIndex::new(id.0 as usize)
874}
875
876fn to_id(index: NodeIndex) -> BloqNodeId {
877    BloqNodeId(index.index() as u32)
878}
879
880fn to_edge_ref(edge: petgraph::stable_graph::EdgeReference<'_, BloqEdge>) -> BloqEdgeRef<'_> {
881    BloqEdgeRef {
882        source: to_id(edge.source()),
883        target: to_id(edge.target()),
884        edge: edge.weight(),
885    }
886}
887
888/// One `Value` input of a node.
889///
890/// This records the producer wired into `slot` and the edge's role.
891/// [`ValueRole::FeedbackFold`] marks runtime Pauli-frame bookkeeping;
892/// [`ValueRole::ReadoutFold`] carries an earlier corrected parity. Both are
893/// omitted from the new decoder query's physical symptoms.
894// Spec rule WF-11.
895#[derive(Debug, Clone, Copy)]
896pub struct ValueInput<'a> {
897    /// Consumer input slot.
898    pub slot: u32,
899    /// Value producer.
900    pub producer: BloqNodeId,
901    /// Edge provenance.
902    pub role: &'a ValueRole,
903    /// Selected Boolean output, or None for structural recipe composition.
904    pub output: Option<super::ObservableOutput>,
905}
906
907impl ValueInput<'_> {
908    /// The selected runtime bit, absent for recipe composition.
909    pub fn value_ref(self) -> Option<super::ValueRef> {
910        self.output.map(|output| super::ValueRef {
911            node: self.producer,
912            output,
913        })
914    }
915}
916
917impl SubGraph {
918    /// Create an empty graph level.
919    pub fn new() -> Self {
920        Self::default()
921    }
922
923    /// The declared bit result, or `None` for constant false (SEM-RVAL).
924    /// Local consumers do not affect this binding.
925    pub fn value_output(&self) -> Option<super::ValueRef> {
926        self.data.value_output
927    }
928
929    /// Select the body's bit result. Validation checks that the node exists
930    /// and produces a bit (WF-17).
931    pub fn set_value_output(&mut self, output: Option<super::ValueRef>) {
932        Arc::make_mut(&mut self.data).value_output = output;
933    }
934
935    /// Declared boundary-binding producers, independently of the bit result
936    /// and any local consumers. Each id names an `Observable` or region (WF-17).
937    pub fn boundary_outputs(&self) -> &[BloqNodeId] {
938        &self.data.boundary_outputs
939    }
940
941    /// Select the body's boundary bindings. Validation checks producer kinds
942    /// and rejects repeated ids; operator multiplicity within an Observable stays.
943    pub fn set_boundary_outputs(&mut self, outputs: Vec<BloqNodeId>) {
944        Arc::make_mut(&mut self.data).boundary_outputs = outputs;
945    }
946
947    /// Add a node and return its level-local id.
948    pub fn add_node(&mut self, node: BloqNode) -> BloqNodeId {
949        to_id(self.graph_mut().add_node(node))
950    }
951
952    /// Adds an edge `from -> to`. Panics when either endpoint id is stale
953    /// (its node was removed and not reused).
954    ///
955    /// # Panics
956    ///
957    /// Panics if either endpoint is stale.
958    pub fn add_edge(&mut self, from: BloqNodeId, to: BloqNodeId, edge: BloqEdge) {
959        self.graph_mut()
960            .add_edge(to_index(from), to_index(to), edge);
961    }
962
963    /// The node at `id`, or `None` when the id is stale.
964    pub fn node(&self, id: BloqNodeId) -> Option<&BloqNode> {
965        self.graph().node_weight(to_index(id))
966    }
967
968    /// Mutable access to the node at `id`, or `None` when the id is stale — the
969    /// mutable pair of [`Self::node`]. Rewrites a node's kind in place (e.g.
970    /// rewriting a region predicate) without disturbing its edges.
971    pub fn node_mut(&mut self, id: BloqNodeId) -> Option<&mut BloqNode> {
972        self.graph_mut().node_weight_mut(to_index(id))
973    }
974
975    /// Remove the node at `id` (and its incident edges), returning it, or `None`
976    /// when the id is stale. Other node ids are unchanged (the id space is
977    /// stable). Used to excise a classical sink — e.g. a dynamically-corrected
978    /// observable the static fold cannot resolve — without renumbering.
979    /// Declarations referencing this id are cleared along with incident edges.
980    /// Rewrites preserving an output must retarget it before removal.
981    pub fn remove_node(&mut self, id: BloqNodeId) -> Option<BloqNode> {
982        let data = Arc::make_mut(&mut self.data);
983        let node = data.graph.remove_node(to_index(id))?;
984        if data.value_output.is_some_and(|value| value.node == id) {
985            data.value_output = None;
986        }
987        data.boundary_outputs.retain(|&output| output != id);
988        Some(node)
989    }
990
991    /// Every node id at this level, in ascending id order.
992    pub fn node_ids(&self) -> impl DoubleEndedIterator<Item = BloqNodeId> + '_ {
993        self.graph().node_indices().map(to_id)
994    }
995
996    /// Every `(id, node)` at this level, in ascending id order.
997    pub fn nodes(&self) -> impl DoubleEndedIterator<Item = (BloqNodeId, &BloqNode)> + '_ {
998        self.graph()
999            .node_indices()
1000            .map(|index| (to_id(index), &self.graph()[index]))
1001    }
1002
1003    /// This level's quantum (block-bearing) nodes, skipping the classical and
1004    /// region nodes that observable lowering and control flow introduce.
1005    pub fn quantum_nodes(&self) -> impl Iterator<Item = (BloqNodeId, &QuantumNode)> + '_ {
1006        self.nodes()
1007            .filter_map(|(id, node)| Some((id, node.try_quantum()?)))
1008    }
1009
1010    /// Number of live nodes.
1011    pub fn node_count(&self) -> usize {
1012        self.graph().node_count()
1013    }
1014
1015    /// Number of live edges.
1016    pub fn edge_count(&self) -> usize {
1017        self.graph().edge_count()
1018    }
1019
1020    /// Every edge at this level.
1021    pub fn edges(&self) -> impl Iterator<Item = BloqEdgeRef<'_>> + '_ {
1022        self.graph().edge_references().map(to_edge_ref)
1023    }
1024
1025    /// Rewrite edge payloads without changing node identities or connectivity.
1026    pub fn for_each_edge_mut(&mut self, mut edit: impl FnMut(&mut BloqEdge)) {
1027        for edge in self.graph_mut().edge_weights_mut() {
1028            edit(edge);
1029        }
1030    }
1031
1032    /// The edges into `id`. Empty for a stale id.
1033    pub fn incoming(&self, id: BloqNodeId) -> impl Iterator<Item = BloqEdgeRef<'_>> + '_ {
1034        self.graph()
1035            .edges_directed(to_index(id), Direction::Incoming)
1036            .map(to_edge_ref)
1037    }
1038
1039    /// The edges out of `id`. Empty for a stale id.
1040    pub fn outgoing(&self, id: BloqNodeId) -> impl Iterator<Item = BloqEdgeRef<'_>> + '_ {
1041        self.graph()
1042            .edges_directed(to_index(id), Direction::Outgoing)
1043            .map(to_edge_ref)
1044    }
1045
1046    /// Every edge `from -> to` (parallel edges are legal).
1047    pub fn edges_between(
1048        &self,
1049        from: BloqNodeId,
1050        to: BloqNodeId,
1051    ) -> impl Iterator<Item = BloqEdgeRef<'_>> + '_ {
1052        self.graph()
1053            .edges_connecting(to_index(from), to_index(to))
1054            .map(to_edge_ref)
1055    }
1056
1057    /// Whether a directed path `from -> to` exists (any edge kind).
1058    pub fn has_path(&self, from: BloqNodeId, to: BloqNodeId) -> bool {
1059        petgraph::algo::has_path_connecting(self.graph(), to_index(from), to_index(to), None)
1060    }
1061
1062    /// Fresh scratch for [`Self::has_path_with_scratch`], sized to this level.
1063    pub fn path_scratch(&self) -> PathScratch {
1064        PathScratch(petgraph::algo::DfsSpace::new(self.graph()))
1065    }
1066
1067    /// [`Self::has_path`] with caller-provided scratch: a batch of probes shares
1068    /// one visit-map allocation instead of re-allocating per call. Each probe
1069    /// still walks the graph (the map is reset, not memoized).
1070    pub fn has_path_with_scratch(
1071        &self,
1072        from: BloqNodeId,
1073        to: BloqNodeId,
1074        scratch: &mut PathScratch,
1075    ) -> bool {
1076        petgraph::algo::has_path_connecting(
1077            self.graph(),
1078            to_index(from),
1079            to_index(to),
1080            Some(&mut scratch.0),
1081        )
1082    }
1083
1084    /// Search a consumer's ancestors without walking its producers' other
1085    /// descendants. Quantum measurement dependencies often precede large
1086    /// classical fanouts, which cannot help establish the required order.
1087    pub(crate) fn has_path_backwards_with_scratch(
1088        &self,
1089        from: BloqNodeId,
1090        to: BloqNodeId,
1091        scratch: &mut PathScratch,
1092    ) -> bool {
1093        petgraph::algo::has_path_connecting(
1094            petgraph::visit::Reversed(self.graph()),
1095            to_index(to),
1096            to_index(from),
1097            Some(&mut scratch.0),
1098        )
1099    }
1100
1101    /// The producers wired into `id`'s `Value` slots.
1102    /// Unordered; collect into a map or sort by slot as needed.
1103    // Spec rule WF-11.
1104    pub fn value_inputs(&self, id: BloqNodeId) -> impl Iterator<Item = ValueInput<'_>> + '_ {
1105        self.incoming(id).filter_map(|edge| match edge.edge {
1106            BloqEdge::Value { slot, role, output } => Some(ValueInput {
1107                slot: *slot,
1108                producer: edge.source,
1109                role,
1110                output: Some(*output),
1111            }),
1112            _ => None,
1113        })
1114    }
1115
1116    /// Data operands, excluding a node's separate activation slot.
1117    pub fn data_inputs(&self, id: BloqNodeId) -> impl Iterator<Item = ValueInput<'_>> + '_ {
1118        self.incoming(id).filter_map(move |edge| {
1119            let (slot, role, output) = match edge.edge {
1120                BloqEdge::Value { slot, role, output } => (*slot, role, Some(*output)),
1121                BloqEdge::Compose { slot, role } => (*slot, role, None),
1122                _ => return None,
1123            };
1124            (Some(slot) != self[id].activation).then_some(ValueInput {
1125                slot,
1126                producer: edge.source,
1127                role,
1128                output,
1129            })
1130        })
1131    }
1132
1133    /// The consumers of `id`'s single `Value` output: `(consumer, slot)`.
1134    pub fn value_consumers(&self, id: BloqNodeId) -> impl Iterator<Item = (BloqNodeId, u32)> + '_ {
1135        self.outgoing(id).filter_map(|edge| match edge.edge {
1136            BloqEdge::Value { slot, .. } => Some((edge.target, *slot)),
1137            _ => None,
1138        })
1139    }
1140
1141    /// Runtime and recipe consumers, sharing the operand-slot namespace.
1142    pub fn data_consumers(&self, id: BloqNodeId) -> impl Iterator<Item = (BloqNodeId, u32)> + '_ {
1143        self.outgoing(id).filter_map(|edge| match edge.edge {
1144            BloqEdge::Value { slot, .. } | BloqEdge::Compose { slot, .. } => {
1145                Some((edge.target, *slot))
1146            }
1147            _ => None,
1148        })
1149    }
1150
1151    /// Consumers including this node's recipe content.
1152    pub fn compose_consumers(
1153        &self,
1154        id: BloqNodeId,
1155    ) -> impl Iterator<Item = (BloqNodeId, u32)> + '_ {
1156        self.outgoing(id).filter_map(|edge| match edge.edge {
1157            BloqEdge::Compose { slot, .. } => Some((edge.target, *slot)),
1158            _ => None,
1159        })
1160    }
1161
1162    /// This level's open `Value` producers: value-producing classical or
1163    /// region nodes whose `Value` no in-level consumer reads. This diagnostic
1164    /// query is independent of the declared outputs (WF-17/SEM-RVAL).
1165    pub fn open_value_producers(&self) -> impl Iterator<Item = BloqNodeId> + '_ {
1166        self.nodes()
1167            .filter(|(id, node)| {
1168                (node.try_region().is_some()
1169                    || matches!(
1170                        node.try_classical(),
1171                        Some(ClassicalNode::Compute { .. } | ClassicalNode::Observable { .. })
1172                    ))
1173                    && self.value_consumers(*id).next().is_none()
1174            })
1175            .map(|(id, _)| id)
1176    }
1177
1178    /// Open producers with a corrected Boolean output.
1179    /// This diagnostic query does not select the declared bit result.
1180    pub fn open_bit_producers(&self) -> impl Iterator<Item = BloqNodeId> + '_ {
1181        self.open_value_producers()
1182    }
1183
1184    /// The body's deterministic Kahn topological order — the same schedule
1185    /// [`crate::Bloq::deterministic_emit_order`] gives the top level, for
1186    /// walkers that execute or emit a region body.
1187    ///
1188    /// # Errors
1189    ///
1190    /// Returns [`crate::CycleDetected`] if the body has a cycle.
1191    pub fn deterministic_emit_order(&self) -> Result<Vec<BloqNodeId>, crate::CycleDetected> {
1192        super::program::deterministic_emit_order_of(self.graph())
1193    }
1194
1195    /// Whether the body is acyclic — [`Self::deterministic_emit_order`]'s
1196    /// success condition, without materializing the order.
1197    pub fn is_acyclic(&self) -> bool {
1198        super::program::is_acyclic_of(self.graph(), &[])
1199    }
1200
1201    /// Whether the body stays acyclic when `extra` directed edges are added
1202    /// (duplicates and parallel edges are allowed). One O(V+E) answer for
1203    /// callers probing candidate insertions, instead of reachability walks
1204    /// or a graph copy per candidate.
1205    pub fn is_acyclic_with_edges(&self, extra: &[(BloqNodeId, BloqNodeId)]) -> bool {
1206        super::program::is_acyclic_of(self.graph(), extra)
1207    }
1208
1209    pub(crate) fn graph(&self) -> &StableDiGraph<BloqNode, BloqEdge> {
1210        &self.data.graph
1211    }
1212
1213    pub(crate) fn graph_mut(&mut self) -> &mut StableDiGraph<BloqNode, BloqEdge> {
1214        &mut Arc::make_mut(&mut self.data).graph
1215    }
1216}
1217
1218/// The panicking twin of [`SubGraph::node`]: panics on a stale id, for passes
1219/// that hold ids they know are live.
1220impl Index<BloqNodeId> for SubGraph {
1221    type Output = BloqNode;
1222
1223    fn index(&self, id: BloqNodeId) -> &Self::Output {
1224        self.node(id)
1225            .expect("BloqNodeId is live in this graph level (spec rule SEM-ID)")
1226    }
1227}