1use 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 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 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 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 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 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 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 || 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 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 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 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 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 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 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 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 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 (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#[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 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
765fn 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 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
822fn 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#[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 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 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 #[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 (
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 (
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 (
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 #[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}