Skip to main content

bloq_ir/
optimize.rs

1//! Classical graph rewrites preserve recipe occurrences and selected output ports.
2
3use std::sync::Arc;
4
5use petgraph::Direction;
6use petgraph::stable_graph::{EdgeIndex, NodeIndex, StableDiGraph};
7use petgraph::visit::{EdgeIndexable, EdgeRef, NodeIndexable};
8
9use crate::{
10    Bloq, BloqEdge, BloqNode, BloqNodeId, BloqNodeKind, ClassicalExpr, ClassicalNode,
11    CycleDetected, InstanceBoundaryOperator, SubGraph, ValueRole,
12};
13
14impl Bloq {
15    /// Fuse computations and compatible observable recipes, then keep only the
16    /// contributor frontier of observable ordering dependencies per level.
17    /// Acyclicity is checked before any rewrite.
18    ///
19    /// # Errors
20    ///
21    /// Returns [`CycleDetected`] without rewriting anything if any level has
22    /// a cycle.
23    pub fn optimize(&mut self) -> Result<(), CycleDetected> {
24        if !self.levels().all(|(_, level)| level.is_acyclic()) {
25            return Err(CycleDetected);
26        }
27        self.top_mut().optimize_level(None);
28        self.top_mut().share_classical_data();
29        Ok(())
30    }
31}
32
33impl SubGraph {
34    /// JSON decodes each nested level separately; do not revisit its children.
35    pub(crate) fn share_local_classical_data(&mut self) {
36        let mut definitions = crate::FxSet::default();
37        for node in self.graph_mut().node_weights_mut() {
38            if let BloqNodeKind::Classical(data) = &mut node.kind {
39                share_definition(data, &mut definitions);
40            }
41        }
42    }
43
44    /// Share exact classical bodies while retaining each node's own execution,
45    /// bindings, activation, and provenance. The pool ends after this traversal;
46    /// nodes own the shared definitions and mutable access detaches them.
47    pub(crate) fn share_classical_data(&mut self) {
48        fn share(level: &mut SubGraph, definitions: &mut crate::FxSet<Arc<ClassicalNode>>) {
49            for node in level.graph_mut().node_weights_mut() {
50                match &mut node.kind {
51                    BloqNodeKind::Classical(data) => {
52                        share_definition(data, definitions);
53                    }
54                    BloqNodeKind::Region(region) => {
55                        for (_, body) in region.bodies_mut() {
56                            share(body, definitions);
57                        }
58                    }
59                    BloqNodeKind::Quantum(_) => {}
60                }
61            }
62        }
63        share(self, &mut crate::FxSet::default());
64    }
65
66    /// Run the optimization passes over this level, then recurse into every
67    /// region body. Passes run before the recursion so a body is optimized as
68    /// its final (post-merge) self, independent of its parent.
69    fn optimize_level(&mut self, restart_source: Option<BloqNodeId>) {
70        let mut pinned = self.rewrite_anchors(restart_source);
71        pinned.extend(self.boundary_outputs());
72        self.fuse_single_use_computes(&pinned);
73        loop {
74            let count = self.graph().node_count();
75            self.fuse_local_observable_fragments(&pinned);
76            if self.graph().node_count() == count {
77                break;
78            }
79        }
80        self.reduce_observable_order_frontiers();
81        for node in self.graph_mut().node_weights_mut() {
82            let classical = match &mut node.kind {
83                BloqNodeKind::Classical(data)
84                    if matches!(data.as_ref(),
85                        ClassicalNode::Compute { expr } | ClassicalNode::Discard { condition: expr }
86                        if expr.needs_affine_compaction())
87                        || matches!(data.as_ref(), ClassicalNode::Observable { .. }) =>
88                {
89                    Some(Arc::make_mut(data))
90                }
91                _ => None,
92            };
93            match classical {
94                Some(
95                    ClassicalNode::Compute { expr } | ClassicalNode::Discard { condition: expr },
96                ) => {
97                    expr.compact_affine();
98                }
99                Some(ClassicalNode::Observable { operators, .. }) => {
100                    canonicalize_operators(operators);
101                }
102                _ => {}
103            }
104            if let BloqNodeKind::Region(region) = &mut node.kind {
105                let crate::RegionNode::RepeatUntilSuccess {
106                    restart_source,
107                    restart_condition,
108                    ..
109                } = region;
110                restart_condition.compact_affine();
111                let restart_source = restart_source.map(|source| source.node);
112                for (_, body) in region.bodies_mut() {
113                    body.optimize_level(restart_source);
114                }
115            }
116        }
117        self.share_readout_recipes();
118    }
119
120    /// Share ordered readout inputs after payload merging has selected the
121    /// final leaves. Discard levels retain their implicit execution barriers.
122    fn share_readout_recipes(&mut self) {
123        if self
124            .nodes()
125            .any(|(_, node)| matches!(node.try_classical(), Some(ClassicalNode::Discard { .. })))
126        {
127            return;
128        }
129        let graph = self.graph();
130        // Binding products follow incoming adjacency, not input-slot order.
131        // Batch rebuilding retains original edge-index order; require both
132        // orders to agree, even on consumers that this pass will not rewrite.
133        for target in graph.node_indices() {
134            if !matches!(
135                graph[target].try_classical(),
136                Some(ClassicalNode::Observable { .. })
137            ) {
138                continue;
139            }
140            let mut previous = None;
141            for edge in graph.edges_directed(target, Direction::Incoming) {
142                if let BloqEdge::Compose { slot, .. } = edge.weight() {
143                    let current = (*slot, edge.id());
144                    if previous.is_some_and(|(slot, index)| current.0 >= slot || current.1 >= index)
145                    {
146                        return;
147                    }
148                    previous = Some(current);
149                }
150            }
151        }
152        let mut recipes = ReadoutRecipes::default();
153        for target in graph.node_indices() {
154            let node = &graph[target];
155            if node.activation.is_some()
156                || !matches!(node.try_classical(), Some(ClassicalNode::Observable { .. }))
157                // Composition slots share a namespace with value operands.
158                // Leave mixed consumers intact, including selected output ports.
159                || graph.edges_directed(target, Direction::Incoming)
160                    .any(|edge| matches!(edge.weight(), BloqEdge::Value { .. }))
161            {
162                continue;
163            }
164            let mut inputs = graph
165                .edges_directed(target, Direction::Incoming)
166                .filter_map(|edge| match edge.weight() {
167                    BloqEdge::Compose { slot, role } => {
168                        Some((*slot, edge.source(), role.clone(), edge.id()))
169                    }
170                    _ => None,
171                })
172                .collect::<Vec<_>>();
173            if inputs.len() < 2 || u32::try_from(inputs.len()).is_err() {
174                continue;
175            }
176            inputs.reverse();
177            recipes.add(target, inputs);
178        }
179        recipes.rewrite(self);
180    }
181
182    /// Inline leaf recipes into compatible consumers, or merge siblings with
183    /// identical activation and composition uses. Preserve occurrences and owners.
184    fn fuse_local_observable_fragments(&mut self, pinned: &crate::FxSet<BloqNodeId>) {
185        let activation = |level: &Self, id| {
186            level[id].activation.map(|slot| {
187                level
188                    .value_inputs(id)
189                    .find(|input| input.slot == slot)
190                    .expect("activation has a value input")
191                    .value_ref()
192                    .expect("runtime input")
193            })
194        };
195        let mut siblings = crate::FxMap::default();
196        let candidates = self
197            .nodes()
198            .filter_map(|(source, node)| {
199                if pinned.contains(&source)
200                    || !matches!(
201                        node.try_classical(),
202                        Some(ClassicalNode::Observable { index: None, .. })
203                    )
204                    || self.data_inputs(source).next().is_some()
205                {
206                    return None;
207                }
208                let mut targets = Vec::new();
209                for edge in self.outgoing(source) {
210                    if !matches!(
211                        edge.edge,
212                        BloqEdge::Compose {
213                            role: ValueRole::Data,
214                            ..
215                        }
216                    ) || !matches!(
217                        self[edge.target].try_classical(),
218                        Some(ClassicalNode::Observable { .. })
219                    ) {
220                        return None;
221                    }
222                    targets.push(edge.target);
223                }
224                if targets.is_empty() {
225                    return None;
226                }
227                let active = activation(self, source);
228                if targets
229                    .iter()
230                    .all(|&target| activation(self, target) == active)
231                {
232                    return Some((source, targets));
233                }
234                // Stamped recipes retain their source identity. Siblings may
235                // use different slot numbers but must have equal use counts.
236                if node.provenance != crate::NodeProvenance::None {
237                    return None;
238                }
239                targets.sort_unstable();
240                match siblings.entry((active, targets)) {
241                    std::collections::hash_map::Entry::Vacant(entry) => {
242                        entry.insert(source);
243                        None
244                    }
245                    std::collections::hash_map::Entry::Occupied(entry) => {
246                        Some((source, vec![*entry.get()]))
247                    }
248                }
249            })
250            .collect::<Vec<_>>();
251        if candidates.is_empty() {
252            return;
253        }
254        let mut orders = self
255            .edges()
256            .filter(|edge| matches!(edge.edge, BloqEdge::Order))
257            .map(|edge| (edge.source, edge.target))
258            .collect::<crate::FxSet<_>>();
259        let mut removed = crate::FxSet::default();
260        let mut rewired = Vec::new();
261        for (source, targets) in candidates {
262            let Some(ClassicalNode::Observable {
263                measurements,
264                operators,
265                ..
266            }) = self
267                .node_mut(source)
268                .expect("candidate remains live")
269                .try_classical_mut()
270            else {
271                unreachable!()
272            };
273            let local_measurements = std::mem::take(measurements);
274            let local_operators = std::mem::take(operators);
275            for target in targets {
276                for edge in self
277                    .incoming(source)
278                    .filter(|edge| matches!(edge.edge, BloqEdge::Order))
279                {
280                    if orders.insert((edge.source, target)) {
281                        rewired.push((
282                            NodeIndex::new(edge.source.0 as usize),
283                            NodeIndex::new(target.0 as usize),
284                            BloqEdge::Order,
285                        ));
286                    }
287                }
288                let Some(ClassicalNode::Observable {
289                    measurements,
290                    operators,
291                    ..
292                }) = self
293                    .node_mut(target)
294                    .expect("consumer remains live")
295                    .try_classical_mut()
296                else {
297                    unreachable!()
298                };
299                measurements.extend_from_slice(&local_measurements);
300                operators.extend_from_slice(&local_operators);
301            }
302            removed.insert(NodeIndex::new(source.0 as usize));
303        }
304        let graph = self.graph_mut();
305        let retained = detach_retained_edges(graph, |_, source, target| {
306            !removed.contains(&source) && !removed.contains(&target)
307        });
308        for &node in &removed {
309            graph.remove_node(node);
310        }
311        for (source, target, edge) in retained.chain(rewired) {
312            graph.add_edge(source, target, edge);
313        }
314    }
315
316    /// Direct unconditional successors already enforce their predecessors' order.
317    /// Each contributor scans only its outgoing adjacency; guarded execution is
318    /// retained because selecting a successor need not execute its predecessor.
319    fn reduce_observable_order_frontiers(&mut self) {
320        let successors = self
321            .quantum_nodes()
322            .filter(|(_, q)| q.guards.is_empty())
323            .map(|(source, _)| {
324                let next = self
325                    .outgoing(source)
326                    .filter_map(|edge| {
327                        (self[edge.target]
328                            .try_quantum()
329                            .is_some_and(|q| q.guards.is_empty())
330                            && match edge.edge {
331                                BloqEdge::Order => true,
332                                BloqEdge::Quantum(q) => q.guard.is_none(),
333                                _ => false,
334                            })
335                        .then_some(edge.target)
336                    })
337                    .collect::<Vec<_>>();
338                (source, next)
339            })
340            .collect::<crate::FxMap<_, _>>();
341        let mut redundant = crate::FxSet::default();
342        for (observable, node) in self.nodes() {
343            if !matches!(node.try_classical(), Some(ClassicalNode::Observable { .. })) {
344                continue;
345            }
346            let mut contributors = crate::FxMap::<BloqNodeId, Vec<EdgeIndex>>::default();
347            for edge in self
348                .graph()
349                .edges_directed(NodeIndex::new(observable.0 as usize), Direction::Incoming)
350                .filter(|edge| matches!(edge.weight(), BloqEdge::Order))
351            {
352                contributors
353                    .entry(BloqNodeId(edge.source().index() as u32))
354                    .or_default()
355                    .push(edge.id());
356            }
357            for (&source, orders) in &contributors {
358                if successors
359                    .get(&source)
360                    .is_some_and(|next| next.iter().any(|target| contributors.contains_key(target)))
361                {
362                    redundant.extend(orders.iter().copied());
363                }
364            }
365        }
366        let graph = self.graph_mut();
367        for edge in redundant {
368            graph.remove_edge(edge);
369        }
370    }
371
372    /// Contract single-use compute forests, then rebuild adjacency once.
373    /// Per-node deletion scans shared neighbors' edge lists quadratically.
374    fn fuse_single_use_computes(&mut self, pinned: &crate::FxSet<BloqNodeId>) {
375        let mut parents = self
376            .graph()
377            .node_indices()
378            .filter_map(|node| {
379                self.fusable_compute_consumer(node, pinned)
380                    .map(|consumer| (node, consumer))
381            })
382            .collect::<crate::FxMap<_, _>>();
383        if parents.is_empty() {
384            return;
385        }
386        // A nonlinear expression can read one slot repeatedly. Keep that
387        // producer shared instead of duplicating its expression tree.
388        let mut uses = parents
389            .values()
390            .map(|&input| (input, 0usize))
391            .collect::<crate::FxMap<_, _>>();
392        let mut consumers = uses.keys().map(|&(node, _)| node).collect::<Vec<_>>();
393        consumers.sort_unstable();
394        consumers.dedup();
395        for consumer in consumers {
396            let Some(ClassicalNode::Compute { expr }) = self.graph()[consumer].try_classical()
397            else {
398                unreachable!("candidate consumer is a Compute")
399            };
400            expr.for_each_input(&mut |slot| {
401                if let Some(count) = uses.get_mut(&(consumer, slot)) {
402                    *count += 1;
403                }
404            });
405        }
406        parents.retain(|_, input| uses[input] == 1);
407        if parents.is_empty() {
408            return;
409        }
410
411        // Accepted links form a forest: each producer has exactly one outgoing
412        // edge, and the graph was checked acyclic before these passes. Order
413        // only its nodes, with children before their consumer.
414        let mut children = crate::FxMap::<NodeIndex, usize>::default();
415        for (&producer, &(consumer, _)) in &parents {
416            children.entry(producer).or_default();
417            *children.entry(consumer).or_default() += 1;
418        }
419        let mut ready = children
420            .iter()
421            .filter_map(|(&node, &count)| (count == 0).then_some(node))
422            .collect::<Vec<_>>();
423        ready.sort_unstable();
424        let mut order = Vec::with_capacity(children.len());
425        while let Some(node) = ready.pop() {
426            order.push(node);
427            if let Some(&(consumer, _)) = parents.get(&node) {
428                let remaining = children
429                    .get_mut(&consumer)
430                    .expect("accepted consumer belongs to the forest");
431                *remaining -= 1;
432                if *remaining == 0 {
433                    ready.push(consumer);
434                }
435            }
436        }
437        assert_eq!(order.len(), children.len(), "accepted links are acyclic");
438        let mut roots = crate::FxMap::default();
439        for &node in order.iter().rev() {
440            if let Some(&(consumer, _)) = parents.get(&node) {
441                let root = roots.get(&consumer).copied().unwrap_or(consumer);
442                roots.insert(node, root);
443                roots.insert(root, root);
444            }
445        }
446
447        let mut expressions = crate::FxMap::default();
448        let mut next_slots = crate::FxMap::<NodeIndex, u32>::default();
449        let mut replaced_inputs = crate::FxSet::default();
450        let mut rewired = Vec::new();
451        for node in order {
452            let Some(&root) = roots.get(&node) else {
453                continue;
454            };
455            let mut inputs = crate::FxMap::default();
456            for edge in self.graph().edges_directed(node, Direction::Incoming) {
457                match edge.weight() {
458                    BloqEdge::Value { slot, role, output } => {
459                        inputs.insert(*slot, (edge.source(), role.clone(), *output, None));
460                        if node == root {
461                            replaced_inputs.insert(edge.id());
462                        }
463                    }
464                    BloqEdge::Order if node != root => {
465                        rewired.push((edge.source(), root, BloqEdge::Order));
466                    }
467                    _ => {}
468                }
469            }
470            let Some(ClassicalNode::Compute { expr }) = self.graph_mut()[node].try_classical_mut()
471            else {
472                unreachable!("fusion forests contain only Computes")
473            };
474            let mut expr = std::mem::replace(expr, ClassicalExpr::Const(false));
475            expr.replace_inputs(&mut |slot| {
476                let (source, role, output, assigned) =
477                    inputs.get_mut(&slot).expect("fed expression slot");
478                if parents.contains_key(source) {
479                    return expressions.remove(source).expect("single expression use");
480                }
481                let assigned = *assigned.get_or_insert_with(|| {
482                    let next = next_slots.entry(root).or_default();
483                    let assigned = *next;
484                    *next = next
485                        .checked_add(1)
486                        .expect("IR edge count fits the slot width");
487                    rewired.push((
488                        *source,
489                        root,
490                        BloqEdge::Value {
491                            slot: assigned,
492                            role: role.clone(),
493                            output: *output,
494                        },
495                    ));
496                    assigned
497                });
498                ClassicalExpr::In(assigned)
499            });
500            expressions.insert(node, expr);
501        }
502        for (node, expr) in expressions {
503            debug_assert!(
504                !parents.contains_key(&node),
505                "a fusion root cannot also be an inlined child"
506            );
507            self.graph_mut()[node].kind =
508                BloqNodeKind::Classical(ClassicalNode::Compute { expr }.into());
509        }
510        let graph = self.graph_mut();
511        let retained = detach_retained_edges(graph, |edge, source, target| {
512            !parents.contains_key(&source)
513                && !parents.contains_key(&target)
514                && !replaced_inputs.contains(&edge)
515        });
516        for &node in parents.keys() {
517            graph.remove_node(node);
518        }
519        for (source, target, edge) in retained.chain(rewired) {
520            graph.add_edge(source, target, edge);
521        }
522    }
523
524    /// Uses outside ordinary graph edges: output frames, named source choices,
525    /// quantum seam guards, the declared result, and the enclosing retry predicate (WF-10).
526    /// Membership and activation inputs already count as ordinary Value uses.
527    fn rewrite_anchors(&self, restart_source: Option<BloqNodeId>) -> crate::FxSet<BloqNodeId> {
528        let mut pinned = self
529            .graph()
530            .node_indices()
531            .filter_map(|index| {
532                matches!(
533                    self.graph()[index].provenance,
534                    crate::NodeProvenance::OutputFrame { .. }
535                        | crate::NodeProvenance::BranchSelector { .. }
536                )
537                .then_some(BloqNodeId(index.index() as u32))
538            })
539            .collect::<crate::FxSet<_>>();
540        pinned.extend(restart_source);
541        pinned.extend(self.value_output().map(|value| value.node));
542        pinned.extend(self.edges().filter_map(|edge| match edge.edge {
543            BloqEdge::Quantum(quantum) => quantum.guard.map(|value| value.node),
544            _ => None,
545        }));
546        pinned
547    }
548
549    /// The sole `Value` consumer and its slot, when both endpoints are
550    /// Computes and the producer is not referenced off-graph.
551    fn fusable_compute_consumer(
552        &self,
553        producer: NodeIndex,
554        pinned: &crate::FxSet<BloqNodeId>,
555    ) -> Option<(NodeIndex, u32)> {
556        let graph = self.graph();
557        if !matches!(
558            graph.node_weight(producer)?.try_classical(),
559            Some(ClassicalNode::Compute { .. })
560        ) || pinned.contains(&BloqNodeId(producer.index() as u32))
561            || graph[producer].activation.is_some()
562        {
563            return None;
564        }
565        let mut outgoing = graph.edges_directed(producer, Direction::Outgoing);
566        let (Some(edge), None) = (outgoing.next(), outgoing.next()) else {
567            return None;
568        };
569        let BloqEdge::Value {
570            slot,
571            role: ValueRole::Data,
572            output: crate::ObservableOutput::Corrected,
573        } = edge.weight()
574        else {
575            return None;
576        };
577        let consumer = edge.target();
578        // SEM-ACTIVATE skips operand evaluation, while a plain Compute evaluates
579        // every operand. Preserve that boundary, including unavailable inputs.
580        (graph[consumer].activation.is_none()
581            && matches!(
582                graph[consumer].try_classical(),
583                Some(ClassicalNode::Compute { .. })
584            ))
585        .then_some((consumer, *slot))
586    }
587}
588
589#[derive(Clone, PartialEq, Eq, Hash)]
590enum ReadoutOperand {
591    Leaf(NodeIndex, ValueRole),
592    Pair(usize),
593}
594
595/// Virtual balanced trees have at most one pair per original input edge.
596/// Only subtrees that save edges become shared, zero-duration IR recipes.
597// ponytail: balanced pairs miss shifted overlaps; broaden matching only when
598// measured savings justify the extra compilation work.
599#[derive(Default)]
600struct ReadoutRecipes {
601    pairs: Vec<([ReadoutOperand; 2], usize)>,
602    interned: crate::FxMap<[ReadoutOperand; 2], usize>,
603    readouts: Vec<(NodeIndex, usize, Vec<EdgeIndex>)>,
604}
605
606impl ReadoutRecipes {
607    fn add(&mut self, target: NodeIndex, inputs: Vec<(u32, NodeIndex, ValueRole, EdgeIndex)>) {
608        let edges = inputs.iter().map(|input| input.3).collect();
609        let mut row = inputs
610            .into_iter()
611            .map(|(_, node, role, _)| ReadoutOperand::Leaf(node, role))
612            .collect::<Vec<_>>();
613        while row.len() > 1 {
614            let mut next = Vec::with_capacity(row.len().div_ceil(2));
615            for chunk in row.chunks(2) {
616                let [left, right] = chunk else {
617                    next.push(chunk[0].clone());
618                    continue;
619                };
620                let key = [left.clone(), right.clone()];
621                let id = if let Some(&id) = self.interned.get(&key) {
622                    id
623                } else {
624                    for child in &key {
625                        if let ReadoutOperand::Pair(child) = child {
626                            self.pairs[*child].1 += 1;
627                        }
628                    }
629                    let id = self.pairs.len();
630                    self.pairs.push((key.clone(), 0));
631                    self.interned.insert(key, id);
632                    id
633                };
634                next.push(ReadoutOperand::Pair(id));
635            }
636            row = next;
637        }
638        let Some(ReadoutOperand::Pair(root)) = row.pop() else {
639            unreachable!("at least two input edges form a pair");
640        };
641        self.pairs[root].1 += 1;
642        self.readouts.push((target, root, edges));
643    }
644
645    fn rewrite(mut self, level: &mut SubGraph) {
646        drop(std::mem::take(&mut self.interned));
647        let mut arities = Vec::<usize>::with_capacity(self.pairs.len());
648        let mut kept = Vec::<bool>::with_capacity(self.pairs.len());
649        for (children, uses) in &self.pairs {
650            let Some(arity) = children.iter().try_fold(0usize, |sum, child| {
651                sum.checked_add(match child {
652                    ReadoutOperand::Pair(id) if !kept[*id] => arities[*id],
653                    _ => 1,
654                })
655            }) else {
656                return;
657            };
658            if u32::try_from(arity).is_err() {
659                return;
660            }
661            // Flattening a parent can increase a child's actual uses. Direct
662            // DAG uses therefore retain only conservative, proven savings.
663            kept.push(arity.saturating_add(*uses) < arity.saturating_mul(*uses));
664            arities.push(arity);
665        }
666        let new_nodes = kept.iter().filter(|&&keep| keep).count();
667        if new_nodes == 0
668            || level
669                .graph()
670                .node_bound()
671                .checked_add(new_nodes)
672                .is_none_or(|bound| bound > u32::MAX as usize)
673        {
674            return;
675        }
676        let rewritten = self
677            .readouts
678            .iter()
679            .enumerate()
680            .filter_map(|(index, (_, root, edges))| {
681                let arity = if kept[*root] { 1 } else { arities[*root] };
682                (arity < edges.len()).then_some((index, arity))
683            })
684            .collect::<Vec<_>>();
685        let Some(new_edges) = arities
686            .iter()
687            .zip(&kept)
688            .filter(|(_, keep)| **keep)
689            .map(|(&arity, _)| arity)
690            .chain(rewritten.iter().map(|(_, arity)| *arity))
691            .try_fold(0usize, usize::checked_add)
692        else {
693            return;
694        };
695        let removed = rewritten
696            .iter()
697            .flat_map(|(index, _)| self.readouts[*index].2.iter().copied())
698            .collect::<crate::FxSet<_>>();
699        if new_edges >= removed.len() {
700            return;
701        }
702        let graph = level.graph_mut();
703        let mut emitted = vec![None; self.pairs.len()];
704        for (id, &keep) in kept.iter().enumerate() {
705            if keep {
706                emitted[id] = Some(graph.add_node(BloqNode::classical(ClassicalNode::fragment())));
707            }
708        }
709        for (source, target, edge) in
710            detach_retained_edges(graph, |edge, _, _| !removed.contains(&edge))
711        {
712            graph.add_edge(source, target, edge);
713        }
714        for (id, (children, _)) in self.pairs.iter().enumerate() {
715            if let Some(target) = emitted[id] {
716                self.wire(graph, target, children, &emitted);
717            }
718        }
719        for (index, _) in rewritten {
720            let (target, root, _) = &self.readouts[index];
721            self.wire(graph, *target, &[ReadoutOperand::Pair(*root)], &emitted);
722        }
723    }
724
725    fn wire(
726        &self,
727        graph: &mut StableDiGraph<BloqNode, BloqEdge>,
728        target: NodeIndex,
729        inputs: &[ReadoutOperand],
730        emitted: &[Option<NodeIndex>],
731    ) {
732        let mut pending = inputs.iter().rev().collect::<Vec<_>>();
733        let mut slot = 0u32;
734        while let Some(input) = pending.pop() {
735            let (source, role) = match input {
736                ReadoutOperand::Leaf(source, role) => (*source, role.clone()),
737                ReadoutOperand::Pair(id) => {
738                    if let Some(source) = emitted[*id] {
739                        (source, ValueRole::Data)
740                    } else {
741                        pending.extend(self.pairs[*id].0.iter().rev());
742                        continue;
743                    }
744                }
745            };
746            graph.add_edge(source, target, BloqEdge::Compose { slot, role });
747            slot = slot
748                .checked_add(1)
749                .expect("recipe arity was checked before rewriting");
750        }
751    }
752}
753
754fn share_definition(
755    data: &mut Arc<ClassicalNode>,
756    definitions: &mut crate::FxSet<Arc<ClassicalNode>>,
757) {
758    if let Some(shared) = definitions.get(data) {
759        *data = Arc::clone(shared);
760    } else {
761        definitions.insert(Arc::clone(data));
762    }
763}
764
765/// Move retained payloads out, then clear adjacency before batch node removal.
766/// Removing the now-isolated nodes is O(1); graph allocations remain reusable.
767fn detach_retained_edges<F>(
768    graph: &mut StableDiGraph<BloqNode, BloqEdge>,
769    mut retain: F,
770) -> impl Iterator<Item = (NodeIndex, NodeIndex, BloqEdge)> + use<F>
771where
772    F: FnMut(EdgeIndex, NodeIndex, NodeIndex) -> bool,
773{
774    // Most edges are plain Data inputs. Keep their endpoints and slot in
775    // twelve bytes instead of copying the full edge enum for every entry.
776    // Other payloads retain their ordinal in a sparse side list. Rebuilding
777    // in this order leaves dense edge slots, just like decoding the program;
778    // incident-only deletion would make later edits reuse different slots.
779    let mut retained = Vec::with_capacity(graph.edge_count());
780    let mut other = Vec::new();
781    for index in 0..graph.edge_bound() {
782        let edge = EdgeIndex::new(index);
783        let Some((source, target)) = graph.edge_endpoints(edge) else {
784            continue;
785        };
786        if retain(edge, source, target) {
787            let weight = std::mem::replace(
788                graph
789                    .edge_weight_mut(edge)
790                    .expect("edge endpoints came from the same live edge"),
791                BloqEdge::Order,
792            );
793            let slot = match weight {
794                BloqEdge::Value {
795                    slot,
796                    role: ValueRole::Data,
797                    output: crate::ObservableOutput::Corrected,
798                } => slot,
799                weight => {
800                    other.push((retained.len(), weight));
801                    0
802                }
803            };
804            retained.push((source, target, slot));
805        }
806    }
807    graph.clear_edges();
808    let mut other = other.into_iter().peekable();
809    retained
810        .into_iter()
811        .enumerate()
812        .map(move |(index, (source, target, slot))| {
813            let weight = if other.peek().is_some_and(|(ordinal, _)| *ordinal == index) {
814                other.next().expect("peeked retained edge").1
815            } else {
816                BloqEdge::value(slot)
817            };
818            (source, target, weight)
819        })
820}
821
822/// Canonicalize boundary bindings while preserving multiplicity.
823fn canonicalize_operators(operators: &mut [InstanceBoundaryOperator]) {
824    operators.sort_by_cached_key(|op| {
825        (
826            op.instance.0,
827            op.face as u8,
828            op.operator
829                .iter()
830                .map(|(coord, pauli)| (coord.x, coord.y, *pauli as u8))
831                .collect::<Vec<_>>(),
832        )
833    });
834}
835
836/// Read a Compute expression for regression checks.
837#[cfg(test)]
838fn compute_expr(kind: &BloqNodeKind) -> ClassicalExpr {
839    match kind.try_classical() {
840        Some(ClassicalNode::Compute { expr }) => expr.clone(),
841        _ => unreachable!("fusion only ever inlines Compute nodes"),
842    }
843}
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848    use crate::{InstanceMeasurement, TemplateInstanceId};
849    use petgraph::visit::IntoEdgeReferences;
850
851    fn expanded_readout_inputs(
852        bloq: &Bloq,
853        target: BloqNodeId,
854        by_slot: bool,
855    ) -> Vec<(BloqNodeId, ValueRole)> {
856        let inputs = |node| {
857            let mut inputs = bloq
858                .data_inputs(node)
859                .filter(|input| input.output.is_none())
860                .collect::<Vec<_>>();
861            if by_slot {
862                inputs.sort_unstable_by_key(|input| input.slot);
863            }
864            inputs
865                .into_iter()
866                .map(|input| (input.producer, input.role.clone()))
867                .collect::<Vec<_>>()
868        };
869        let mut pending = inputs(target).into_iter().rev().collect::<Vec<_>>();
870        let mut leaves = Vec::new();
871        while let Some((node, role)) = pending.pop() {
872            if matches!(
873                bloq[node].try_classical(),
874                Some(ClassicalNode::Observable { index: None, measurements, operators }) if measurements.is_empty() && operators.is_empty()
875            ) {
876                assert_eq!(role, ValueRole::Data);
877                pending.extend(inputs(node).into_iter().rev());
878            } else {
879                leaves.push((node, role));
880            }
881        }
882        leaves
883    }
884
885    #[test]
886    fn shared_readout_recipes_preserve_leaves_roles_payloads_and_owners() {
887        let mut bloq = Bloq::new();
888        let leaves = (0..3)
889            .map(|index| bloq.add_node(BloqNode::classical(ClassicalNode::observable(100 + index))))
890            .collect::<Vec<_>>();
891        bloq.node_mut(leaves[1]).unwrap().activation = Some(0);
892        bloq.add_edge(leaves[2], leaves[1], BloqEdge::value(0));
893        let inputs = vec![
894            (leaves[0], ValueRole::Data),
895            (leaves[1], ValueRole::FeedbackFold { action: 7 }),
896            (leaves[0], ValueRole::ReadoutFold),
897            (leaves[2], ValueRole::Data),
898            (leaves[0], ValueRole::Data),
899            (leaves[1], ValueRole::FeedbackFold { action: 7 }),
900            (leaves[0], ValueRole::ReadoutFold),
901        ];
902        let mut changed_role = inputs.clone();
903        changed_role[1].1 = ValueRole::Data;
904        let cases = [
905            inputs.clone(),
906            inputs.clone(),
907            inputs.clone(),
908            changed_role,
909            Vec::new(),
910            vec![inputs[0].clone()],
911        ];
912        let mut observables = Vec::new();
913        for (index, inputs) in cases.iter().enumerate() {
914            let node = bloq.add_node(
915                BloqNode::classical(ClassicalNode::Observable {
916                    index: Some(index as u32),
917                    measurements: vec![InstanceMeasurement {
918                        instance: TemplateInstanceId(0),
919                        measurement: index as u32,
920                    }],
921                    operators: Vec::new(),
922                })
923                .with_provenance(crate::NodeProvenance::Generator {
924                    ordinal: index as u32,
925                }),
926            );
927            for (slot, (producer, role)) in inputs.iter().enumerate() {
928                bloq.add_edge(
929                    *producer,
930                    node,
931                    BloqEdge::Compose {
932                        slot: 2 * slot as u32,
933                        role: role.clone(),
934                    },
935                );
936            }
937            observables.push(node);
938        }
939        bloq.node_mut(observables[0]).unwrap().activation = Some(99);
940        bloq.add_edge(leaves[2], observables[0], BloqEdge::value(99));
941        let payloads = observables
942            .iter()
943            .map(|&node| bloq[node].try_classical().unwrap().clone())
944            .collect::<Vec<_>>();
945        let edges = bloq.edge_count();
946        let activated_slots = value_slots_into(&bloq, observables[0]);
947        let adjacency = observables
948            .iter()
949            .map(|&node| expanded_readout_inputs(&bloq, node, false))
950            .collect::<Vec<_>>();
951        bloq.top_mut().share_readout_recipes();
952        assert!(bloq.edge_count() < edges);
953        assert!(
954            bloq.nodes().any(|(_, node)| matches!(
955                node.try_classical(),
956                Some(ClassicalNode::Observable { index: None, measurements, operators }) if measurements.is_empty() && operators.is_empty()
957            ))
958        );
959        assert_eq!(bloq[leaves[1]].activation, Some(0));
960        assert_eq!(bloq[observables[0]].activation, Some(99));
961        assert_eq!(value_slots_into(&bloq, observables[0]), activated_slots);
962        for (index, (&node, expected)) in observables.iter().zip(&cases).enumerate() {
963            assert_eq!(bloq[node].try_classical(), Some(&payloads[index]));
964            assert_eq!(
965                bloq[node].provenance,
966                crate::NodeProvenance::Generator {
967                    ordinal: index as u32
968                }
969            );
970            assert_eq!(expanded_readout_inputs(&bloq, node, true), *expected);
971            assert_eq!(
972                expanded_readout_inputs(&bloq, node, false),
973                adjacency[index]
974            );
975        }
976    }
977
978    #[test]
979    fn shared_readout_recipes_require_savings_and_skip_discard_levels() {
980        for (width, uses, discard, expected_recipes) in [
981            (1, 4, false, 0),
982            (2, 2, false, 0),
983            (4, 2, false, 1),
984            (4, 3, true, 0),
985        ] {
986            let mut bloq = Bloq::new();
987            let inputs = (0..width)
988                .map(|_| bloq.add_node(BloqNode::classical(ClassicalNode::observable(100))))
989                .collect::<Vec<_>>();
990            for index in 0..uses {
991                let node = bloq.add_node(BloqNode::classical(ClassicalNode::observable(index)));
992                for (slot, &source) in inputs.iter().enumerate() {
993                    bloq.add_edge(source, node, BloqEdge::compose(slot as u32));
994                }
995            }
996            if discard {
997                bloq.add_node(BloqNode::classical(ClassicalNode::Discard {
998                    condition: ClassicalExpr::Const(false),
999                }));
1000            }
1001            let before = bloq.to_binary();
1002            let edges = bloq.edge_count();
1003            bloq.top_mut().share_readout_recipes();
1004            assert_eq!(
1005                bloq.nodes()
1006                    .filter(|(_, node)| matches!(
1007                        node.try_classical(),
1008                        Some(ClassicalNode::Observable { index: None, measurements, operators }) if measurements.is_empty() && operators.is_empty()
1009                    ))
1010                    .count(),
1011                expected_recipes
1012            );
1013            assert!(bloq.edge_count() <= edges);
1014            if expected_recipes == 0 {
1015                assert_eq!(bloq.to_binary(), before);
1016            }
1017        }
1018    }
1019
1020    #[test]
1021    fn readout_sharing_preserves_shuffled_signed_binding_order() {
1022        let mut bloq = Bloq::new();
1023        let leaves = [
1024            bloq_circuit::Pauli::X,
1025            bloq_circuit::Pauli::X,
1026            bloq_circuit::Pauli::Z,
1027        ]
1028        .into_iter()
1029        .map(|pauli| {
1030            bloq.add_node(BloqNode::classical(ClassicalNode::Observable {
1031                index: None,
1032                measurements: Vec::new(),
1033                operators: vec![InstanceBoundaryOperator {
1034                    instance: TemplateInstanceId(0),
1035                    face: crate::BoundaryFace::Output,
1036                    operator: [(glam::IVec2::ZERO, pauli)].into_iter().collect(),
1037                }],
1038            }))
1039        })
1040        .collect::<Vec<_>>();
1041        let mut observables = Vec::new();
1042        for index in 0..2 {
1043            let node = bloq.add_node(BloqNode::classical(ClassicalNode::observable(index)));
1044            for (source, slot) in leaves.iter().zip([0, 2, 1]) {
1045                bloq.add_edge(*source, node, BloqEdge::compose(slot));
1046            }
1047            observables.push(node);
1048        }
1049        // Adjacency multiplies Z * X * X = Z. Slot-sorted regrouping would
1050        // multiply X * Z * X = -Z, so this level must remain untouched.
1051        let expected = vec![
1052            (leaves[2], ValueRole::Data),
1053            (leaves[1], ValueRole::Data),
1054            (leaves[0], ValueRole::Data),
1055        ];
1056        let before = bloq.to_binary();
1057        bloq.top_mut().share_readout_recipes();
1058        assert_eq!(bloq.to_binary(), before);
1059        for node in observables {
1060            assert_eq!(expanded_readout_inputs(&bloq, node, false), expected);
1061        }
1062    }
1063
1064    #[test]
1065    fn readout_sharing_skips_edge_reuse_on_unmodified_consumers() {
1066        for activated in [false, true] {
1067            let mut bloq = Bloq::new();
1068            let leaves = (0..4)
1069                .map(|_| bloq.add_node(BloqNode::classical(ClassicalNode::observable(100))))
1070                .collect::<Vec<_>>();
1071            let mut untouched = BloqNode::classical(if activated {
1072                ClassicalNode::observable(0)
1073            } else {
1074                ClassicalNode::fragment()
1075            });
1076            if activated {
1077                untouched.activation = Some(99);
1078            }
1079            let untouched = bloq.add_node(untouched);
1080            if activated {
1081                bloq.add_edge(leaves[0], untouched, BloqEdge::value(99));
1082            }
1083            let index = |node: BloqNodeId| NodeIndex::new(node.0 as usize);
1084            let graph = bloq.top_mut().graph_mut();
1085            let hole = graph.add_edge(index(leaves[0]), index(leaves[1]), BloqEdge::Order);
1086            let first = graph.add_edge(index(leaves[0]), index(untouched), BloqEdge::compose(0));
1087            graph.remove_edge(hole);
1088            let reused = graph.add_edge(index(leaves[1]), index(untouched), BloqEdge::compose(1));
1089            assert!(reused < first);
1090            // These other, canonical observables would otherwise share a recipe.
1091            for ordinal in 1..4 {
1092                let node = bloq.add_node(BloqNode::classical(ClassicalNode::observable(ordinal)));
1093                for (slot, &source) in leaves.iter().enumerate() {
1094                    bloq.add_edge(source, node, BloqEdge::compose(slot as u32));
1095                }
1096            }
1097            let before_inputs = bloq
1098                .data_inputs(untouched)
1099                .map(|input| (input.producer, input.slot))
1100                .collect::<Vec<_>>();
1101            assert_eq!(
1102                before_inputs
1103                    .iter()
1104                    .map(|input| input.1)
1105                    .collect::<Vec<_>>(),
1106                [1, 0]
1107            );
1108            let before = bloq.to_binary();
1109            bloq.top_mut().share_readout_recipes();
1110            assert_eq!(bloq.to_binary(), before);
1111            assert_eq!(
1112                bloq.data_inputs(untouched)
1113                    .map(|input| (input.producer, input.slot))
1114                    .collect::<Vec<_>>(),
1115                before_inputs
1116            );
1117        }
1118    }
1119
1120    #[test]
1121    fn recipe_sharing_preserves_mixed_value_ports_and_composition_slots() {
1122        let mut program = Bloq::new();
1123        let leaves = (0..4)
1124            .map(|index| program.add_node(BloqNode::classical(ClassicalNode::observable(index))))
1125            .collect::<Vec<_>>();
1126        let mut targets = Vec::new();
1127        for index in 4..7 {
1128            let target = program.add_node(BloqNode::classical(ClassicalNode::observable(index)));
1129            for (slot, &source) in leaves.iter().enumerate() {
1130                program.add_edge(source, target, BloqEdge::compose(slot as u32));
1131            }
1132            targets.push(target);
1133        }
1134        let mixed = targets[2];
1135        program.add_edge(leaves[0], mixed, BloqEdge::flip(4));
1136        program.validate().unwrap();
1137        let before = program.clone();
1138        let edges = program.edge_count();
1139        let inputs = program
1140            .data_inputs(mixed)
1141            .map(|input| (input.producer, input.slot, input.output))
1142            .collect::<Vec<_>>();
1143
1144        program.top_mut().share_readout_recipes();
1145
1146        program.validate().unwrap();
1147        assert!(program.edge_count() < edges);
1148        assert_eq!(
1149            program
1150                .data_inputs(mixed)
1151                .map(|input| (input.producer, input.slot, input.output))
1152                .collect::<Vec<_>>(),
1153            inputs
1154        );
1155        for target in targets {
1156            for value in [
1157                target.into(),
1158                crate::ValueRef {
1159                    node: target,
1160                    output: crate::ObservableOutput::Flip,
1161                },
1162            ] {
1163                let assignment = crate::ClassicalAssignment::Uniform(false);
1164                assert_eq!(
1165                    program.resolve_value(value, assignment),
1166                    before.resolve_value(value, assignment)
1167                );
1168                assert_eq!(
1169                    program.measurement_value_dependencies(value, assignment),
1170                    before.measurement_value_dependencies(value, assignment)
1171                );
1172            }
1173        }
1174    }
1175
1176    #[test]
1177    fn retained_edge_scratch_preserves_weights_and_dense_future_edge_ids() {
1178        let mut bloq = Bloq::new();
1179        let nodes = (0..4)
1180            .map(|_| {
1181                bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1182                    expr: ClassicalExpr::Const(false),
1183                }))
1184            })
1185            .collect::<Vec<_>>();
1186        let index = |node: BloqNodeId| NodeIndex::new(node.0 as usize);
1187        let expected = vec![
1188            (index(nodes[0]), index(nodes[1]), BloqEdge::value(u32::MAX)),
1189            (index(nodes[1]), index(nodes[2]), BloqEdge::Order),
1190            (
1191                index(nodes[2]),
1192                index(nodes[3]),
1193                BloqEdge::Value {
1194                    slot: u32::MAX - 1,
1195                    role: ValueRole::FeedbackFold { action: u32::MAX },
1196                    output: crate::ObservableOutput::Corrected,
1197                },
1198            ),
1199            (
1200                index(nodes[0]),
1201                index(nodes[3]),
1202                BloqEdge::Value {
1203                    slot: 0,
1204                    role: ValueRole::ReadoutFold,
1205                    output: crate::ObservableOutput::Corrected,
1206                },
1207            ),
1208            (
1209                index(nodes[1]),
1210                index(nodes[3]),
1211                BloqEdge::Quantum(Box::new(crate::QuantumEdge {
1212                    pipes: Vec::new(),
1213                    guard: Some(nodes[0].into()),
1214                })),
1215            ),
1216        ];
1217        let graph = bloq.top_mut().graph_mut();
1218        graph.add_edge(expected[0].0, expected[0].1, expected[0].2.clone());
1219        let skipped = graph.add_edge(index(nodes[0]), index(nodes[2]), BloqEdge::value(9));
1220        for (source, target, edge) in &expected[1..] {
1221            graph.add_edge(*source, *target, edge.clone());
1222        }
1223        let hole = graph.add_edge(index(nodes[3]), index(nodes[2]), BloqEdge::Order);
1224        graph.remove_edge(hole);
1225        for (source, target, edge) in detach_retained_edges(graph, |id, _, _| id != skipped) {
1226            graph.add_edge(source, target, edge);
1227        }
1228        assert_eq!(graph.edge_bound(), expected.len());
1229        assert_eq!(
1230            graph
1231                .edge_references()
1232                .map(|edge| (edge.source(), edge.target(), edge.weight().clone()))
1233                .collect::<Vec<_>>(),
1234            expected
1235        );
1236
1237        let mut decoded = Bloq::from_binary(&bloq.to_binary()).unwrap();
1238        let added = (index(nodes[3]), index(nodes[0]), BloqEdge::value(17));
1239        let live_id = bloq
1240            .top_mut()
1241            .graph_mut()
1242            .add_edge(added.0, added.1, added.2.clone());
1243        let decoded_id = decoded
1244            .top_mut()
1245            .graph_mut()
1246            .add_edge(added.0, added.1, added.2);
1247        assert_eq!(live_id, decoded_id);
1248        assert_eq!(bloq.to_binary(), decoded.to_binary());
1249    }
1250
1251    fn observable(bloq: &mut Bloq) -> BloqNodeId {
1252        bloq.add_node(BloqNode::classical(ClassicalNode::observable(0)))
1253    }
1254
1255    fn fragment(bloq: &mut Bloq, measurement: u32) -> BloqNodeId {
1256        bloq.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
1257            vec![crate::InstanceMeasurement {
1258                instance: TemplateInstanceId(0),
1259                measurement,
1260            }],
1261            Vec::new(),
1262        )))
1263    }
1264
1265    fn value_slots_into(bloq: &Bloq, node: BloqNodeId) -> Vec<u32> {
1266        let mut slots: Vec<u32> = bloq.value_inputs(node).map(|input| input.slot).collect();
1267        slots.sort_unstable();
1268        slots
1269    }
1270
1271    #[test]
1272    fn classical_definitions_share_without_merging_bindings_or_activation() {
1273        fn definition(bloq: &Bloq, id: u32) -> &Arc<ClassicalNode> {
1274            let BloqNodeKind::Classical(data) = &bloq[BloqNodeId(id)].kind else {
1275                panic!("expected a classical node")
1276            };
1277            data
1278        }
1279
1280        let mut bloq = Bloq::from_text(
1281            r#"BLOQIR 1
1282template t0 {
1283  circuit {
1284    R (0,0)
1285    M (0,0):m0
1286  }
1287}
1288graph {
1289  n0 quantum {
1290    instance i0 t0 @ (0,0)
1291  }
1292  n1 observable fragment measurements i0:m0
1293  n2 observable fragment measurements i0:m0 when v0
1294  n3 observable 0
1295  n4 compute in0
1296  n5 compute in0 when v1
1297  n0 -> n1 order
1298  n0 -> n2 order
1299  n3 -> n2 value 0
1300  n1 -> n4 value 0
1301  n2 -> n5 value 0
1302  n3 -> n5 value 1
1303}
1304"#,
1305        )
1306        .unwrap();
1307        bloq.validate().unwrap();
1308        let before = bloq.to_text();
1309
1310        bloq.optimize().unwrap();
1311
1312        bloq.validate().unwrap();
1313        assert_eq!(bloq.to_text(), before);
1314        assert!(Arc::ptr_eq(definition(&bloq, 1), definition(&bloq, 2)));
1315        assert!(Arc::ptr_eq(definition(&bloq, 4), definition(&bloq, 5)));
1316        let original = Arc::clone(definition(&bloq, 5));
1317        let Some(ClassicalNode::Compute { expr }) =
1318            bloq.node_mut(BloqNodeId(4)).unwrap().try_classical_mut()
1319        else {
1320            panic!("the edited node remains a Compute")
1321        };
1322        *expr = ClassicalExpr::Const(true);
1323        assert!(!Arc::ptr_eq(definition(&bloq, 4), &original));
1324        assert!(Arc::ptr_eq(definition(&bloq, 5), &original));
1325        assert_eq!(
1326            definition(&bloq, 5).as_ref(),
1327            &ClassicalNode::Compute {
1328                expr: ClassicalExpr::In(0)
1329            }
1330        );
1331    }
1332
1333    #[test]
1334    fn cyclic_program_is_rejected_unchanged() {
1335        let mut bloq = Bloq::new();
1336        let a = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1337            expr: ClassicalExpr::In(0),
1338        }));
1339        let b = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1340            expr: ClassicalExpr::In(0),
1341        }));
1342        bloq.add_edge(a, b, BloqEdge::value(0));
1343        bloq.add_edge(b, a, BloqEdge::value(0));
1344        assert_eq!(bloq.optimize(), Err(CycleDetected));
1345        assert_eq!((bloq.top().node_count(), bloq.top().edge_count()), (2, 2));
1346    }
1347
1348    #[test]
1349    fn guarded_rewrites_preserve_recipes_effects_and_external_uses() {
1350        let mut bloq = Bloq::from_text(
1351            r#"BLOQIR 1
1352template t0 {
1353  circuit {
1354    R (0,0)
1355    M (0,0):m0
1356    M (0,0):m1
1357  }
1358}
1359graph {
1360  n0 observable 0
1361  n1 compute in0 from selector choice
1362  n2 compute in0
1363  n3 compute in0
1364  n4 quantum {
1365    instance i0 t0 @ (0,0)
1366  }
1367  n5 quantum {
1368    instance i1 t0 @ (2,0)
1369    guard 7 i1
1370  }
1371  n6 observable fragment measurements i1:m0 when v0
1372  n7 observable fragment measurements i1:m1 when v1
1373  n8 observable fragment measurements i0:m0
1374  n9 observable fragment measurements i0:m1 when v2
1375  n10 observable 1 when v90
1376  n11 observable fragment operators i1 input X(2,0) when v4
1377  n12 observable fragment operators i1 output Z(2,0) when v5
1378  n13 compute !in0
1379  n14 compute !in9
1380  n15 compute in0 when v1
1381  n16 compute !in0
1382  n17 compute !in0
1383  n18 compute in0 when v1
1384  n19 observable fragment measurements i0:m1
1385  n21 rus in0 when v9 {
1386    body {
1387      n0 compute 1
1388      n1 compute !in0
1389      n0 -> n1 value 0
1390    }
1391  }
1392  n22 compute !in0
1393  n23 compute in0 ^ in1
1394  n24 compute in0 & in0
1395  n0 -> n1 value 0
1396  n1 -> n2 value 0
1397  n2 -> n3 value 0
1398  n3 -> n5 value 7
1399  n3 -> n6 value 0
1400  n3 -> n7 value 1
1401  n3 -> n10 value 90
1402  n3 -> n11 value 4
1403  n3 -> n12 value 5
1404  n3 -> n22 value 0
1405  n22 -> n9 value 2
1406  n5 -> n6 order
1407  n5 -> n7 order
1408  n5 -> n11 order
1409  n5 -> n12 order
1410  n4 -> n8 order
1411  n4 -> n9 order
1412  n6 -> n10 compose 2
1413  n7 -> n10 compose 5
1414  n8 -> n10 compose 6
1415  n9 -> n10 compose 7
1416  n11 -> n10 compose 8
1417  n12 -> n10 compose 9
1418  n0 -> n13 value 0
1419  n13 -> n14 value 9
1420  n0 -> n15 value 0
1421  n3 -> n15 value 1
1422  n15 -> n16 value 0
1423  n0 -> n17 value 0
1424  n17 -> n18 value 0
1425  n3 -> n18 value 1
1426  n4 -> n19 order
1427  n19 -> n10 value 12 readout
1428  n0 -> n21 value 0
1429  n3 -> n21 value 9
1430  n0 -> n23 value 0
1431  n3 -> n23 value 1
1432  n23 -> n24 value 0
1433}
1434"#,
1435        )
1436        .unwrap();
1437        bloq.add_edge(
1438            BloqNodeId(4),
1439            BloqNodeId(5),
1440            BloqEdge::Quantum(Box::new(crate::QuantumEdge {
1441                pipes: Vec::new(),
1442                guard: Some(BloqNodeId(2).into()),
1443            })),
1444        );
1445        bloq.validate().unwrap();
1446        let before = bloq.clone();
1447        bloq.optimize().expect("acyclic test program");
1448        bloq.validate().unwrap();
1449
1450        assert!(bloq.node(BloqNodeId(13)).is_none());
1451        for retained in [1, 2, 8, 9, 15, 17, 19, 23] {
1452            assert!(bloq.node(BloqNodeId(retained)).is_some(), "n{retained}");
1453        }
1454        assert_eq!(
1455            bloq[BloqNodeId(23)].try_classical(),
1456            Some(&ClassicalNode::Compute {
1457                expr: ClassicalExpr::parity([0, 1], false)
1458            })
1459        );
1460        assert_eq!(
1461            bloq.top()
1462                .value_inputs(BloqNodeId(10))
1463                .find(|input| Some(input.slot) == bloq[BloqNodeId(10)].activation)
1464                .unwrap()
1465                .producer,
1466            BloqNodeId(3)
1467        );
1468        assert!(bloq.top().data_inputs(BloqNodeId(10)).any(|input| {
1469            input.producer == BloqNodeId(19) && *input.role == ValueRole::ReadoutFold
1470        }));
1471        let body = bloq[BloqNodeId(21)]
1472            .try_region()
1473            .unwrap()
1474            .bodies()
1475            .next()
1476            .unwrap()
1477            .1;
1478        assert_eq!(body.node_count(), 1);
1479
1480        for optimized in [
1481            Bloq::from_binary(&bloq.to_binary()).unwrap(),
1482            Bloq::from_text(&bloq.to_text()).unwrap(),
1483        ] {
1484            optimized.validate().unwrap();
1485            for selected in [false, true] {
1486                let values = [(0, selected)].into();
1487                let assignment = crate::ClassicalAssignment::Pinned {
1488                    forced_observables: &values,
1489                };
1490                for output in [10, 14, 16, 18, 21] {
1491                    assert_eq!(
1492                        optimized.resolve_classical(BloqNodeId(output), assignment),
1493                        before.resolve_classical(BloqNodeId(output), assignment),
1494                        "n{output}, choice={selected}"
1495                    );
1496                }
1497                let choices = [("choice".into(), selected)].into();
1498                let old = before.pin_membership(&choices).unwrap();
1499                let new = optimized.pin_membership(&choices).unwrap();
1500                assert!(!new.has_conditional_membership());
1501                for quantum in [4, 5] {
1502                    assert_eq!(
1503                        old[BloqNodeId(quantum)].expect_quantum().instances,
1504                        new[BloqNodeId(quantum)].expect_quantum().instances,
1505                    );
1506                }
1507            }
1508        }
1509    }
1510
1511    /// Fusing a producer used to leave a hole at its consumer slot
1512    /// (`Xor(In(2), In(1))`, no slot 0). The renumber sweep must leave the
1513    /// fused consumer with dense `0..n` edge slots and matching `In` leaves.
1514    #[test]
1515    fn fused_consumer_slots_are_dense() {
1516        let mut bloq = Bloq::new();
1517        let a = fragment(&mut bloq, 0);
1518        let b = fragment(&mut bloq, 1);
1519        let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1520            expr: ClassicalExpr::Xor(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
1521        }));
1522        bloq.add_edge(a, producer, BloqEdge::value(0));
1523        bloq.add_edge(b, producer, BloqEdge::value(1));
1524        let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1525            expr: ClassicalExpr::In(0),
1526        }));
1527        bloq.add_edge(producer, consumer, BloqEdge::value(0));
1528
1529        bloq.top_mut().fuse_single_use_computes(&Default::default());
1530
1531        let slots = value_slots_into(&bloq, consumer);
1532        assert_eq!(slots, vec![0, 1], "edge slots renumber to dense 0..n");
1533        let Some(ClassicalNode::Compute { expr }) = bloq[consumer].try_classical() else {
1534            panic!("consumer stays a Compute");
1535        };
1536        let mut leaves = Vec::new();
1537        expr.for_each_input(&mut |slot| leaves.push(slot));
1538        leaves.sort_unstable();
1539        assert_eq!(leaves, vec![0, 1], "In leaves reference the dense slots");
1540    }
1541
1542    #[test]
1543    fn fusion_preserves_shared_direct_and_indirect_input() {
1544        let mut bloq = Bloq::new();
1545        let source = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1546            expr: ClassicalExpr::Const(true),
1547        }));
1548        let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1549            expr: ClassicalExpr::In(0),
1550        }));
1551        bloq.add_edge(source, producer, BloqEdge::value(0));
1552        let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1553            expr: ClassicalExpr::Xor(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
1554        }));
1555        bloq.add_edge(source, consumer, BloqEdge::value(0));
1556        bloq.add_edge(producer, consumer, BloqEdge::value(1));
1557
1558        bloq.top_mut().fuse_single_use_computes(&Default::default());
1559
1560        assert!(bloq.node(producer).is_none());
1561        assert_eq!(value_slots_into(&bloq, consumer), vec![0, 1]);
1562        let Some(ClassicalNode::Compute { expr }) = bloq[consumer].try_classical() else {
1563            panic!("consumer stays a Compute");
1564        };
1565        assert_eq!(expr.eval(&mut |_| Some(true)), Some(false));
1566        bloq.validate().unwrap();
1567    }
1568
1569    #[test]
1570    fn fusion_handles_the_max_consumer_slot_without_overflow() {
1571        let mut bloq = Bloq::new();
1572        let producer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1573            expr: ClassicalExpr::Xor(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
1574        }));
1575        let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1576            expr: ClassicalExpr::Xor(Box::new([
1577                ClassicalExpr::In(0),
1578                ClassicalExpr::Xor(Box::new([
1579                    ClassicalExpr::In(u32::MAX - 1),
1580                    ClassicalExpr::In(u32::MAX),
1581                ])),
1582            ])),
1583        }));
1584        let source = observable(&mut bloq);
1585        bloq.add_edge(source, producer, BloqEdge::value(0));
1586        bloq.add_edge(source, producer, BloqEdge::value(1));
1587        bloq.add_edge(source, consumer, BloqEdge::value(0));
1588        bloq.add_edge(source, consumer, BloqEdge::value(u32::MAX - 1));
1589        bloq.add_edge(producer, consumer, BloqEdge::value(u32::MAX));
1590        bloq.validate().expect("max slot is valid IR");
1591
1592        bloq.optimize().expect("acyclic test program");
1593
1594        bloq.validate().expect("optimization leaves valid IR");
1595    }
1596
1597    #[test]
1598    fn fusion_compacts_sparse_slots_through_compute_forests() {
1599        for (graph, expected) in [
1600            // A constant and a value meet at the maximum input slot.
1601            (
1602                "n0 compute in0\nn1 compute 1\nn2 compute in0 ^ in4294967295\nn3 observable 0\nn3 -> n0 value 0\nn0 -> n2 value 0\nn1 -> n2 value 4294967295",
1603                true,
1604            ),
1605            // A forest can cross a sparse intermediate interface.
1606            (
1607                "n0 compute in0 ^ in1\nn1 compute in4294967295\nn2 compute in0\nn3 observable 0\nn3 -> n0 value 0\nn3 -> n0 value 1\nn0 -> n1 value 4294967295\nn1 -> n2 value 0",
1608                false,
1609            ),
1610            // An internal constant removes one leaf from the final interface.
1611            (
1612                "n0 compute in0 ^ in1\nn1 compute 1\nn2 compute in0 ^ in4294967294\nn3 observable 0\nn1 -> n0 value 0\nn3 -> n0 value 1\nn0 -> n2 value 0\nn3 -> n2 value 4294967294",
1613                true,
1614            ),
1615        ] {
1616            let mut bloq = Bloq::from_text(&format!("BLOQIR 1\ngraph {{\n{graph}\n}}")).unwrap();
1617            bloq.validate().unwrap();
1618            bloq.optimize().expect("acyclic test program");
1619            bloq.validate().unwrap();
1620            assert!(bloq.node(BloqNodeId(0)).is_none(), "{graph}");
1621            assert!(bloq.node(BloqNodeId(1)).is_none(), "{graph}");
1622            assert_eq!(
1623                compute_expr(&bloq[BloqNodeId(2)].kind).eval(&mut |_| Some(false)),
1624                Some(expected),
1625                "{graph}"
1626            );
1627        }
1628    }
1629
1630    #[test]
1631    fn fusion_orders_disconnected_branching_forests_and_keeps_order_dependencies() {
1632        let mut bloq = Bloq::new();
1633        let compute = |bloq: &mut Bloq, expr| {
1634            bloq.add_node(BloqNode::classical(ClassicalNode::Compute { expr }))
1635        };
1636        let source = compute(&mut bloq, ClassicalExpr::Const(true));
1637        let barrier = compute(&mut bloq, ClassicalExpr::Const(false));
1638        let left = compute(&mut bloq, ClassicalExpr::In(0));
1639        let right = compute(
1640            &mut bloq,
1641            ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
1642        );
1643        let joined = compute(
1644            &mut bloq,
1645            ClassicalExpr::And(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(1)])),
1646        );
1647        let single = compute(&mut bloq, ClassicalExpr::In(0));
1648        let single_root = compute(
1649            &mut bloq,
1650            ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
1651        );
1652        let repeated = compute(&mut bloq, ClassicalExpr::In(0));
1653        let repeated_root = compute(
1654            &mut bloq,
1655            ClassicalExpr::And(Box::new([ClassicalExpr::In(0), ClassicalExpr::In(0)])),
1656        );
1657        for node in [left, right, single, repeated] {
1658            bloq.add_edge(source, node, BloqEdge::value(0));
1659        }
1660        bloq.add_edge(barrier, left, BloqEdge::Order);
1661        bloq.add_edge(left, joined, BloqEdge::value(0));
1662        bloq.add_edge(right, joined, BloqEdge::value(1));
1663        bloq.add_edge(single, single_root, BloqEdge::value(0));
1664        bloq.add_edge(repeated, repeated_root, BloqEdge::value(0));
1665        bloq.validate().unwrap();
1666        let before = bloq.clone();
1667
1668        bloq.top_mut().fuse_single_use_computes(&Default::default());
1669
1670        for removed in [left, right, single] {
1671            assert!(bloq.node(removed).is_none());
1672        }
1673        assert!(bloq.node(repeated).is_some(), "repeated slot blocks fusion");
1674        assert_eq!(value_slots_into(&bloq, joined), vec![0, 1]);
1675        assert_eq!(value_slots_into(&bloq, single_root), vec![0]);
1676        assert!(
1677            bloq.incoming(joined)
1678                .any(|edge| { edge.source == barrier && matches!(edge.edge, BloqEdge::Order) })
1679        );
1680        bloq.validate().unwrap();
1681        let values = Default::default();
1682        let assignment = crate::ClassicalAssignment::Pinned {
1683            forced_observables: &values,
1684        };
1685        for node in [joined, single_root, repeated_root] {
1686            assert_eq!(
1687                before.resolve_classical(node, assignment),
1688                bloq.resolve_classical(node, assignment)
1689            );
1690        }
1691    }
1692
1693    #[test]
1694    fn optimize_preserves_every_rus_restart_source_value() {
1695        for body in [
1696            "n0 compute 0\nn1 compute !in0\nn0 -> n1 value 0",
1697            "n0 observable fragment\nn1 observable fragment\nn2 observable 0\nn0 -> n2 compose 0\nn1 -> n2 compose 1",
1698        ] {
1699            for source in [0, 1] {
1700                let mut bloq = Bloq::from_text(&format!(
1701                    "BLOQIR 1\ngraph {{\nn0 rus in0 source n{source} {{\nbody {{\n{body}\n}}\n}}\n}}"
1702                ))
1703                .unwrap();
1704                bloq.validate().unwrap();
1705                let path =
1706                    crate::LevelPath::default().child(BloqNodeId(0), crate::BodySelector::Body);
1707                let original = bloq.level_at(&path).unwrap()[BloqNodeId(source)]
1708                    .try_classical()
1709                    .unwrap()
1710                    .clone();
1711
1712                bloq.optimize().expect("acyclic test program");
1713
1714                bloq.validate().unwrap();
1715                let current = bloq.level_at(&path).unwrap()[BloqNodeId(source)]
1716                    .try_classical()
1717                    .unwrap();
1718                match (&original, current) {
1719                    (
1720                        ClassicalNode::Compute { expr: before },
1721                        ClassicalNode::Compute { expr: after },
1722                    ) => assert_eq!(
1723                        before.eval(&mut |_| Some(false)),
1724                        after.eval(&mut |_| Some(false))
1725                    ),
1726                    _ => assert_eq!(current, &original),
1727                }
1728            }
1729        }
1730    }
1731
1732    /// An `OutputFrame`-stamped `Compute` is referenced off-graph (the frame
1733    /// table derives from stamps), so fusion must never inline it — even when
1734    /// its only edge is one `Value` into another `Compute`. The derived table
1735    /// keeps naming the surviving nodes.
1736    #[test]
1737    fn output_frame_stamped_compute_survives_fusion() {
1738        use crate::{Basis, FramePair, NodeProvenance};
1739        use glam::ivec3;
1740
1741        let mut bloq = Bloq::new();
1742        let stamped = |basis| {
1743            BloqNode::classical(ClassicalNode::Compute {
1744                expr: ClassicalExpr::Const(false),
1745            })
1746            .with_provenance(NodeProvenance::OutputFrame {
1747                port: ivec3(0, 0, 1),
1748                basis,
1749            })
1750        };
1751        let frame_x = bloq.add_node(stamped(Basis::X));
1752        let frame_z = bloq.add_node(stamped(Basis::Z));
1753        let consumer = bloq.add_node(BloqNode::classical(ClassicalNode::Compute {
1754            expr: ClassicalExpr::In(0),
1755        }));
1756        bloq.add_edge(frame_x, consumer, BloqEdge::value(0));
1757
1758        bloq.optimize().expect("acyclic test program");
1759
1760        assert_eq!(
1761            bloq.output_frames(),
1762            vec![FramePair {
1763                port: ivec3(0, 0, 1),
1764                x: frame_x,
1765                z: frame_z,
1766            }],
1767            "the stamped frame pair survives fusion and derives intact"
1768        );
1769    }
1770
1771    #[test]
1772    fn observable_order_frontier_keeps_incomparable_and_guarded_contributors() {
1773        let mut program = Bloq::new();
1774        let a = program.add_node(BloqNode::from_members(Vec::new()));
1775        let b = program.add_node(BloqNode::from_members(Vec::new()));
1776        let c = program.add_node(BloqNode::from_members(Vec::new()));
1777        let observable = program.add_node(BloqNode::classical(ClassicalNode::observable(0)));
1778        program.add_edge(a, b, BloqEdge::quantum(Vec::new()));
1779        for source in [a, a, b, c] {
1780            program.add_edge(source, observable, BloqEdge::Order);
1781        }
1782        program.optimize().unwrap();
1783        let orders = program
1784            .incoming(observable)
1785            .map(|edge| edge.source)
1786            .collect::<crate::FxSet<_>>();
1787        assert_eq!(orders, [b, c].into_iter().collect());
1788        assert!(program.has_path(a, observable));
1789
1790        let mut guarded = Bloq::new();
1791        let predicate = guarded.add_node(BloqNode::classical(ClassicalNode::Compute {
1792            expr: ClassicalExpr::Const(true),
1793        }));
1794        let a = guarded.add_node(BloqNode::from_members(Vec::new()));
1795        let b = guarded.add_node(BloqNode::from_members(Vec::new()));
1796        let mut observable_node = BloqNode::classical(ClassicalNode::observable(0));
1797        observable_node.activation = Some(8);
1798        let observable = guarded.add_node(observable_node);
1799        guarded.add_edge(predicate, observable, BloqEdge::value(8));
1800        guarded.add_edge(
1801            a,
1802            b,
1803            BloqEdge::Quantum(Box::new(crate::QuantumEdge {
1804                pipes: Vec::new(),
1805                guard: Some(predicate.into()),
1806            })),
1807        );
1808        for source in [a, b] {
1809            guarded.add_edge(source, observable, BloqEdge::Order);
1810        }
1811        guarded.optimize().unwrap();
1812        assert_eq!(
1813            guarded
1814                .incoming(observable)
1815                .filter(|edge| matches!(edge.edge, BloqEdge::Order))
1816                .count(),
1817            2
1818        );
1819        assert_eq!(guarded[observable].activation, Some(8));
1820    }
1821
1822    #[test]
1823    fn shared_observable_leaves_inline_measurements_and_owned_boundaries() {
1824        for payload in [
1825            "measurements i0:m0*i0:m0*i1:m1",
1826            "operators i0 input X(0,0), i1 input X(2,0)",
1827            "measurements i0:m0*i0:m0*i1:m1 operators i0 input X(0,0), i1 input X(2,0)",
1828        ] {
1829            let mut program = Bloq::from_text(&format!(
1830                "BLOQIR 1\ntemplate t0 {{\ncircuit {{\nR (0,0)\nM (0,0):m0\nM (0,0):m1\n}}\n}}\ngraph {{\nn0 quantum {{\ninstance i0 t0 @ (0,0)\ninstance i1 t0 @ (2,0)\n}}\nn1 observable fragment {payload}\nn2 observable 0\nn3 observable 1\nn0 -> n1 order\nn1 -> n2 compose 0\nn1 -> n2 compose 1\nn1 -> n3 compose 0\n}}"
1831            ))
1832            .unwrap();
1833            program.validate().unwrap();
1834            let before = program.clone();
1835            let leaf = before[BloqNodeId(1)].try_classical().unwrap();
1836            program.optimize().unwrap();
1837            assert!(program.node(BloqNodeId(1)).is_none());
1838            for optimized in [
1839                program.clone(),
1840                Bloq::from_binary(&program.to_binary()).unwrap(),
1841                Bloq::from_text(&program.to_text()).unwrap(),
1842            ] {
1843                optimized.validate().unwrap();
1844                for (target, copies) in [(BloqNodeId(2), 2), (BloqNodeId(3), 1)] {
1845                    let actual = optimized[target].try_classical().unwrap();
1846                    assert_eq!(actual.measurements(), leaf.measurements().repeat(copies));
1847                    let mut operators = (0..copies)
1848                        .flat_map(|_| leaf.operators().iter().cloned())
1849                        .collect::<Vec<_>>();
1850                    canonicalize_operators(&mut operators);
1851                    assert_eq!(actual.operators(), operators);
1852                    assert_eq!(
1853                        optimized
1854                            .resolve_classical(target, crate::ClassicalAssignment::Uniform(false)),
1855                        before
1856                            .resolve_classical(target, crate::ClassicalAssignment::Uniform(false))
1857                    );
1858                    assert_eq!(
1859                        optimized.measurement_dependencies(
1860                            target,
1861                            crate::ClassicalAssignment::Uniform(false)
1862                        ),
1863                        before.measurement_dependencies(
1864                            target,
1865                            crate::ClassicalAssignment::Uniform(false)
1866                        )
1867                    );
1868                    assert!(optimized.incoming(target).any(|edge| {
1869                        edge.source == BloqNodeId(0) && matches!(edge.edge, BloqEdge::Order)
1870                    }));
1871                }
1872            }
1873        }
1874    }
1875
1876    #[test]
1877    fn shared_observable_leaves_retain_activation_and_external_uses() {
1878        for extra in [
1879            "n4 compute in0\nn1 -> n4 value 0",
1880            "n4 compute 1\nn4 -> n2 value 9",
1881            "",
1882        ] {
1883            let mut program = Bloq::from_text(&format!(
1884                "BLOQIR 1\ngraph {{\nn1 observable fragment\nn2 observable 0\nn3 observable 1\nn1 -> n2 compose 0\nn1 -> n3 compose 0\n{extra}\n}}"
1885            ))
1886            .unwrap();
1887            if extra.contains("value 9") {
1888                program.node_mut(BloqNodeId(2)).unwrap().activation = Some(9);
1889            } else if extra.is_empty() {
1890                program.top_mut().set_boundary_outputs(vec![BloqNodeId(1)]);
1891            }
1892            program.validate().unwrap();
1893            program.optimize().unwrap();
1894            program.validate().unwrap();
1895            assert!(program.node(BloqNodeId(1)).is_some());
1896        }
1897    }
1898
1899    #[test]
1900    fn sibling_fragments_merge_recipes_with_matching_activation_and_uses() {
1901        let mut program = Bloq::from_text(
1902            "BLOQIR 1
1903template t0 {
1904  circuit {
1905    R (0,0)
1906    M (0,0):m0
1907  }
1908}
1909graph {
1910  n0 quantum {
1911    instance i0 t0 @ (0,0)
1912  }
1913  n1 quantum {
1914    instance i1 t0 @ (2,0)
1915  }
1916  n2 observable 0
1917  n3 observable fragment measurements i0:m0*i0:m0 when v4
1918  n4 observable fragment measurements i1:m0 operators i1 input X(2,0) when v9
1919  n5 observable fragment operators i0 input Z(0,0) when v0
1920  n6 observable fragment when v0
1921  n10 observable 1
1922  n11 observable 2
1923  n0 -> n3 order
1924  n1 -> n4 order
1925  n0 -> n5 order
1926  n2 -> n3 value 4 flip
1927  n2 -> n4 value 9 flip
1928  n2 -> n5 value 0 flip
1929  n2 -> n6 value 0
1930  n3 -> n10 compose 0
1931  n3 -> n10 compose 1
1932  n3 -> n11 compose 0
1933  n4 -> n10 compose 2
1934  n4 -> n10 compose 3
1935  n4 -> n11 compose 1
1936  n5 -> n10 compose 4
1937  n6 -> n10 compose 5
1938}",
1939        )
1940        .unwrap();
1941        program.validate().unwrap();
1942        let before = program.clone();
1943        program.optimize().unwrap();
1944        assert!(program.node(BloqNodeId(4)).is_none());
1945        assert!(
1946            program.node(BloqNodeId(5)).is_some(),
1947            "different use counts"
1948        );
1949        assert!(
1950            program.node(BloqNodeId(6)).is_some(),
1951            "different output port"
1952        );
1953        let merged = program[BloqNodeId(3)].try_classical().unwrap();
1954        assert_eq!(merged.measurements().len(), 3, "retain duplicate terms");
1955        assert_eq!(merged.operators().len(), 1);
1956        assert_eq!(program[BloqNodeId(3)].activation, Some(4));
1957        for source in [BloqNodeId(0), BloqNodeId(1)] {
1958            assert!(program.has_path(source, BloqNodeId(3)));
1959        }
1960        for mut optimized in [
1961            program.clone(),
1962            Bloq::from_text(&program.to_text()).unwrap(),
1963            Bloq::from_binary(&program.to_binary()).unwrap(),
1964        ] {
1965            optimized.validate().unwrap();
1966            for active in [false, true] {
1967                let assignment = crate::ClassicalAssignment::Uniform(active);
1968                for target in [BloqNodeId(10), BloqNodeId(11)] {
1969                    assert_eq!(
1970                        optimized.resolve_classical(target, assignment),
1971                        before.resolve_classical(target, assignment)
1972                    );
1973                    assert_eq!(
1974                        optimized.measurement_dependencies(target, assignment),
1975                        before.measurement_dependencies(target, assignment)
1976                    );
1977                }
1978            }
1979            let text = optimized.to_text();
1980            optimized.optimize().unwrap();
1981            assert_eq!(optimized.to_text(), text, "optimization is idempotent");
1982        }
1983    }
1984
1985    #[test]
1986    fn compute_fusion_retains_flip_port_and_recipe_occurrences() {
1987        let mut program = Bloq::new();
1988        let term = crate::InstanceMeasurement {
1989            instance: TemplateInstanceId(0),
1990            measurement: 0,
1991        };
1992        let fragment = program.add_node(BloqNode::classical(ClassicalNode::observable_fragment(
1993            vec![term, term],
1994            Vec::new(),
1995        )));
1996        let observable = program.add_node(BloqNode::classical(ClassicalNode::observable(0)));
1997        program.add_edge(fragment, observable, BloqEdge::compose(2));
1998        let intermediate = program.add_node(BloqNode::classical(ClassicalNode::Compute {
1999            expr: ClassicalExpr::In(0),
2000        }));
2001        let result = program.add_node(BloqNode::classical(ClassicalNode::Compute {
2002            expr: ClassicalExpr::Not(Box::new(ClassicalExpr::In(0))),
2003        }));
2004        program.add_edge(observable, intermediate, BloqEdge::flip(0));
2005        program.add_edge(intermediate, result, BloqEdge::value(0));
2006        let before = program
2007            .resolve_classical(result, crate::ClassicalAssignment::Uniform(false))
2008            .unwrap();
2009        program.optimize().unwrap();
2010        assert!(program.node(fragment).is_none());
2011        assert_eq!(
2012            program[observable].try_classical().unwrap().measurements(),
2013            [term, term]
2014        );
2015        assert_eq!(
2016            program.data_inputs(result).next().unwrap().output,
2017            Some(crate::ObservableOutput::Flip)
2018        );
2019        assert_eq!(
2020            program
2021                .resolve_classical(result, crate::ClassicalAssignment::Uniform(false))
2022                .unwrap(),
2023            before
2024        );
2025        assert_eq!(program.incoming(observable).count(), 0);
2026    }
2027}