1use petgraph::visit::EdgeRef;
4use std::collections::BTreeMap;
5use thiserror::Error;
6
7use crate::{
8 Bloq, BloqEdge, BloqNodeId, BloqNodeKind, ClassicalExpr, ClassicalNode, CycleDetected, FxMap,
9 FxSet, NodeProvenance, NodeTemplateInstanceMergeError, ObservableOutput, SubGraph, ValueRef,
10};
11
12#[derive(Debug, Error)]
14#[non_exhaustive]
15pub enum MembershipPinError {
16 #[error("{0}")]
18 Cycle(#[from] CycleDetected),
19 #[error("{0}")]
21 InvalidMembership(#[from] NodeTemplateInstanceMergeError),
22 #[error("unknown source branch selector `{0}`")]
24 UnknownSelector(String),
25 #[error("source branch selector `{0}` has no pin")]
27 MissingSelector(String),
28 #[error("source branch selector `{0}` is not unique at the top level")]
30 AmbiguousSelector(String),
31 #[error("source selector pins contradict their Boolean dataflow")]
33 UnreachableAssignment,
34}
35
36impl Bloq {
37 pub fn pin_membership(
51 &self,
52 choices: &BTreeMap<String, bool>,
53 ) -> Result<Self, MembershipPinError> {
54 for (_, level) in self.levels() {
57 if !level.is_acyclic() {
58 return Err(CycleDetected.into());
59 }
60 }
61 let mut selectors = BTreeMap::new();
62 for (path, level) in self.levels() {
63 for (id, node) in level.nodes() {
64 if let NodeProvenance::BranchSelector { name } = &node.provenance
65 && (!path.segments().is_empty() || selectors.insert(name.clone(), id).is_some())
66 {
67 return Err(MembershipPinError::AmbiguousSelector(name.clone()));
68 }
69 }
70 }
71 for name in choices.keys() {
72 if !selectors.contains_key(name) {
73 return Err(MembershipPinError::UnknownSelector(name.clone()));
74 }
75 }
76 let forced = selectors
77 .iter()
78 .map(|(name, &node)| {
79 choices
80 .get(name)
81 .copied()
82 .map(|value| (node.into(), value))
83 .ok_or_else(|| MembershipPinError::MissingSelector(name.clone()))
84 })
85 .collect::<Result<FxMap<_, _>, _>>()?;
86 let mut pinned = self.clone();
87 pin_level(pinned.top_mut(), &forced)?;
88 prune_unused_values(pinned.top_mut());
89 Ok(pinned)
90 }
91
92 pub fn has_conditional_membership(&self) -> bool {
97 fn guarded(level: &SubGraph, under_activation: bool) -> bool {
98 level.nodes().any(|(_, node)| match &node.kind {
99 BloqNodeKind::Quantum(quantum) => under_activation || !quantum.guards.is_empty(),
100 BloqNodeKind::Region(region) => region
101 .bodies()
102 .any(|(_, body)| guarded(body, under_activation || node.activation.is_some())),
103 BloqNodeKind::Classical(_) => false,
104 }) || level.edges().any(
105 |edge| matches!(edge.edge, BloqEdge::Quantum(quantum) if quantum.guard.is_some()),
106 )
107 }
108 guarded(self.top(), false)
109 }
110}
111
112fn membership_input(
113 level: &SubGraph,
114 node: BloqNodeId,
115 slot: u32,
116) -> Result<ValueRef, MembershipPinError> {
117 level
118 .value_inputs(node)
119 .find(|input| input.slot == slot)
120 .map(|input| input.value_ref().expect("runtime input"))
121 .ok_or(NodeTemplateInstanceMergeError::MissingMembershipInput(slot).into())
122}
123
124fn fixed_values(
125 level: &SubGraph,
126 forced: &FxMap<ValueRef, bool>,
127) -> Result<FxMap<ValueRef, bool>, MembershipPinError> {
128 let mut known = forced.clone();
129 if !forced.is_empty() {
130 let mut predicates = forced
131 .iter()
132 .map(|(&id, &bit)| (id, bit))
133 .collect::<Vec<_>>();
134 predicates.sort_unstable();
135 let mut nodes = forced.keys().copied().collect::<FxSet<_>>();
138 for (id, node) in level.nodes() {
139 for input in level.value_inputs(id) {
140 if node.activation == Some(input.slot)
141 || node.try_quantum().is_some_and(|quantum| {
142 quantum.guards.iter().any(|guard| guard.input == input.slot)
143 })
144 {
145 nodes.insert(input.value_ref().expect("runtime input"));
146 }
147 }
148 }
149 nodes.extend(level.edges().filter_map(|edge| match edge.edge {
150 BloqEdge::Quantum(quantum) => quantum.guard,
151 _ => None,
152 }));
153 let mut nodes = nodes.into_iter().collect::<Vec<_>>();
154 nodes.sort_unstable();
155 let implied = crate::membership::PredicateAnalysis::new(level)
156 .values_fixed_by_assignment(&predicates, nodes)?
157 .ok_or(MembershipPinError::UnreachableAssignment)?;
158 known.extend(implied);
159 }
160 Ok(known)
161}
162
163fn known_values(level: &SubGraph, forced: &FxMap<ValueRef, bool>) -> FxMap<ValueRef, bool> {
164 let mut known = forced.clone();
165 for node in level
166 .deterministic_emit_order()
167 .expect("validated acyclic graph")
168 {
169 if known.contains_key(&node.into()) {
170 continue;
171 }
172 let active = level[node]
173 .activation
174 .map(|slot| {
175 level
176 .value_inputs(node)
177 .find(|input| input.slot == slot)
178 .and_then(|input| {
179 known
180 .get(&input.value_ref().expect("runtime input"))
181 .copied()
182 })
183 })
184 .unwrap_or(Some(true));
185 if active == Some(false) {
186 known.insert(node.into(), false);
187 if matches!(
188 level[node].try_classical(),
189 Some(ClassicalNode::Observable { index: Some(_), .. })
190 ) {
191 known.insert(
192 ValueRef {
193 node,
194 output: ObservableOutput::Flip,
195 },
196 false,
197 );
198 }
199 continue;
200 }
201 if let Some(ClassicalNode::Compute { expr }) = level[node].try_classical() {
202 let inputs = level
203 .value_inputs(node)
204 .map(|input| {
205 (
206 input.slot,
207 known
208 .get(&input.value_ref().expect("runtime input"))
209 .copied(),
210 )
211 })
212 .collect();
213 let value = simplified(expr, &inputs).eval(&mut |_| None);
214 if let Some(value) = value
215 && (active == Some(true) || !value)
216 {
217 known.insert(node.into(), value);
218 }
219 }
220 }
221 known
222}
223
224fn pin_level(
225 level: &mut SubGraph,
226 forced: &FxMap<ValueRef, bool>,
227) -> Result<(), MembershipPinError> {
228 let fixed = fixed_values(level, forced)?;
229 let known = known_values(level, &fixed);
230 let mut removed_slots = FxMap::<BloqNodeId, FxSet<u32>>::default();
231 let mut remove_all_inputs = FxSet::default();
232 let mut pinned_observables = FxMap::default();
233 for id in level.node_ids().collect::<Vec<_>>() {
234 let mut node = level[id].clone();
235 if let Some(quantum) = node.try_quantum() {
236 let mut inputs = FxMap::default();
237 for guard in &quantum.guards {
238 let producer = membership_input(level, id, guard.input)?;
239 if let Some(&value) = known.get(&producer) {
240 inputs.insert(guard.input, value);
241 removed_slots.entry(id).or_default().insert(guard.input);
242 }
243 }
244 node = pin_quantum_members(&node, &inputs)?;
245 for input in level.value_inputs(id) {
246 if !node
247 .expect_quantum()
248 .guards
249 .iter()
250 .any(|guard| guard.input == input.slot)
251 {
252 removed_slots.entry(id).or_default().insert(input.slot);
253 }
254 }
255 }
256 if let Some(slot) = node.activation {
257 let producer = membership_input(level, id, slot)?;
258 if let Some(&enabled) = known.get(&producer) {
259 if !enabled
262 && matches!(
263 node.try_classical(),
264 Some(ClassicalNode::Observable { index: Some(_), .. })
265 )
266 {
267 let Some(ClassicalNode::Observable {
268 measurements,
269 operators,
270 ..
271 }) = node.try_classical_mut()
272 else {
273 unreachable!("indexed observable checked above")
274 };
275 measurements.clear();
276 operators.clear();
277 removed_slots
278 .entry(id)
279 .or_default()
280 .extend(level.data_inputs(id).map(|input| input.slot));
281 *level.node_mut(id).expect("live observable") = node;
282 continue;
283 }
284 node.activation = None;
285 removed_slots.entry(id).or_default().insert(slot);
286 if !enabled {
287 let replacement = match node.try_classical() {
288 Some(ClassicalNode::Observable { .. }) => ClassicalNode::fragment(),
289 Some(ClassicalNode::Discard { .. }) => ClassicalNode::Discard {
290 condition: ClassicalExpr::Const(false),
291 },
292 _ => ClassicalNode::Compute {
293 expr: ClassicalExpr::Const(false),
294 },
295 };
296 node.kind = BloqNodeKind::Classical(replacement.into());
297 remove_all_inputs.insert(id);
298 }
299 }
300 }
301 if let Some(&bit) = fixed.get(&id.into())
302 && (forced.contains_key(&id.into())
303 || matches!(node.try_classical(), Some(ClassicalNode::Compute { .. })))
304 {
305 if matches!(
308 node.try_classical(),
309 Some(ClassicalNode::Observable { index: Some(_), .. })
310 ) {
311 pinned_observables.insert(id, bit);
312 } else {
313 node.kind = BloqNodeKind::Classical(
314 ClassicalNode::Compute {
315 expr: ClassicalExpr::Const(bit),
316 }
317 .into(),
318 );
319 node.activation = None;
320 remove_all_inputs.insert(id);
321 }
322 }
323 if let BloqNodeKind::Region(region) = &mut node.kind {
324 for (_, body) in region.bodies_mut() {
325 pin_level(body, &FxMap::default())?;
326 }
327 }
328 if matches!(level[id].kind, BloqNodeKind::Region(_)) && node.try_region().is_none() {
329 let outputs = level
330 .boundary_outputs()
331 .iter()
332 .copied()
333 .filter(|&output| output != id)
334 .collect();
335 level.set_boundary_outputs(outputs);
336 }
337 *level.node_mut(id).expect("live node") = node;
338 }
339 let pinned_observables = pinned_observables
340 .into_iter()
341 .map(|(source, bit)| {
342 let constant = level.add_node(crate::BloqNode::classical(ClassicalNode::Compute {
343 expr: ClassicalExpr::Const(bit),
344 }));
345 (source, constant)
346 })
347 .collect::<FxMap<_, _>>();
348 if let Some(mut result) = level.value_output()
349 && result.output == ObservableOutput::Corrected
350 && let Some(&constant) = pinned_observables.get(&result.node)
351 {
352 result.node = constant;
353 level.set_value_output(Some(result));
354 }
355 for edge in level.graph().edge_indices().collect::<Vec<_>>() {
356 let (source, target) = level.graph().edge_endpoints(edge).expect("live edge");
357 if matches!(
358 level.graph()[edge],
359 BloqEdge::Value {
360 output: ObservableOutput::Corrected,
361 ..
362 }
363 ) && let Some(&constant) = pinned_observables.get(&BloqNodeId(source.index() as u32))
364 {
365 let payload = level.graph_mut().remove_edge(edge).expect("live edge");
366 level.add_edge(constant, BloqNodeId(target.index() as u32), payload);
367 }
368 }
369 let edges = level.graph().edge_indices().collect::<Vec<_>>();
370 for id in edges {
371 let edge = &level.graph()[id];
372 let (_, target) = level.graph().edge_endpoints(id).expect("live edge");
373 let target = BloqNodeId(target.index() as u32);
374 let remove = match edge {
375 BloqEdge::Value { slot, .. } | BloqEdge::Compose { slot, .. } => {
376 remove_all_inputs.contains(&target)
377 || removed_slots
378 .get(&target)
379 .is_some_and(|slots| slots.contains(slot))
380 }
381 BloqEdge::Quantum(quantum) => quantum
382 .guard
383 .is_some_and(|guard| known.get(&guard) == Some(&false)),
384 BloqEdge::Order => false,
385 };
386 if remove {
387 level.graph_mut().remove_edge(id);
388 } else if let BloqEdge::Quantum(quantum) = &mut level.graph_mut()[id]
389 && quantum
390 .guard
391 .is_some_and(|guard| known.contains_key(&guard))
392 {
393 quantum.guard = None;
394 }
395 }
396 let mut seams = FxMap::default();
397 for id in level.graph().edge_indices().collect::<Vec<_>>() {
398 let BloqEdge::Quantum(quantum) = &level.graph()[id] else {
399 continue;
400 };
401 let endpoints = (
402 level.graph().edge_endpoints(id).expect("live edge"),
403 quantum.guard,
404 );
405 if let Some(&previous) = seams.get(&endpoints) {
406 let BloqEdge::Quantum(removed) = level.graph_mut().remove_edge(id).expect("live seam")
407 else {
408 unreachable!("the collected seam edge was checked as quantum")
409 };
410 let BloqEdge::Quantum(kept) = &mut level.graph_mut()[previous] else {
411 unreachable!("the retained seam edge was checked as quantum")
412 };
413 kept.pipes.extend(removed.pipes);
414 } else {
415 seams.insert(endpoints, id);
416 }
417 }
418 fold_constants(level);
419 Ok(())
420}
421
422fn fold_constants(level: &mut SubGraph) {
423 let mut known: FxMap<ValueRef, bool> = FxMap::default();
424 for id in level
425 .deterministic_emit_order()
426 .expect("pinning preserves acyclicity")
427 {
428 let inputs = level
429 .value_inputs(id)
430 .map(|input| {
431 (
432 input.slot,
433 known
434 .get(&input.value_ref().expect("runtime input"))
435 .copied(),
436 )
437 })
438 .collect::<FxMap<_, _>>();
439 let activation = level[id].activation;
440 let expr = match level[id].try_classical() {
441 Some(ClassicalNode::Compute { expr } | ClassicalNode::Discard { condition: expr }) => {
442 Some(simplified(expr, &inputs))
443 }
444 _ => None,
445 };
446 if let Some(expr) = expr {
447 let mut used = FxSet::default();
448 expr.for_each_input(&mut |slot| {
449 used.insert(slot);
450 });
451 match level.node_mut(id).expect("live node").try_classical_mut() {
452 Some(
453 ClassicalNode::Compute { expr: target }
454 | ClassicalNode::Discard { condition: target },
455 ) => *target = expr,
456 _ => unreachable!("the classical expression was checked above"),
457 }
458 used.extend(activation);
459 let discard = level.graph().edges_directed(petgraph::stable_graph::NodeIndex::new(id.0 as usize), petgraph::Direction::Incoming)
460 .filter(|edge| matches!(edge.weight(), BloqEdge::Value { slot, .. } if !used.contains(slot)))
461 .map(|edge| edge.id()).collect::<Vec<_>>();
462 for edge in discard {
463 level.graph_mut().remove_edge(edge);
464 }
465 }
466 if let Some(ClassicalNode::Compute {
467 expr: ClassicalExpr::Const(value),
468 }) = level[id].try_classical()
469 && (!*value || activation.is_none_or(|slot| inputs.get(&slot) == Some(&Some(true))))
470 {
471 known.insert(id.into(), *value);
472 }
473 }
474}
475
476fn pin_quantum_members(
477 node: &crate::BloqNode,
478 inputs: &FxMap<u32, bool>,
479) -> Result<crate::BloqNode, NodeTemplateInstanceMergeError> {
480 if inputs.is_empty() {
481 return Ok(node.clone());
482 }
483 node.select_quantum_members(|_| Some(false))?;
486 let quantum = node.expect_quantum();
487 let renumber = |count, kind: u8| {
488 let removed = quantum
489 .guards
490 .iter()
491 .filter(|guard| inputs.get(&guard.input) == Some(&false))
492 .flat_map(|guard| match kind {
493 0 => &guard.detectors,
494 1 => &guard.detector_bundles,
495 _ => &guard.restarts,
496 })
497 .copied()
498 .collect::<FxSet<_>>();
499 let mut next = 0;
500 (0..count as u32)
501 .map(|index| {
502 if removed.contains(&index) {
503 None
504 } else {
505 let index = next;
506 next += 1;
507 Some(index)
508 }
509 })
510 .collect::<Vec<_>>()
511 };
512 let detectors = renumber(quantum.detectors.len(), 0);
513 let detector_bundles = renumber(quantum.detector_bundles.len(), 1);
514 let restarts = renumber(quantum.restarts.len(), 2);
515 let mut pending = quantum
516 .guards
517 .iter()
518 .filter(|guard| !inputs.contains_key(&guard.input))
519 .cloned()
520 .collect::<Vec<_>>();
521 for guard in &mut pending {
522 guard.detectors = guard
523 .detectors
524 .iter()
525 .filter_map(|&index| detectors[index as usize])
526 .collect();
527 guard.detector_bundles = guard
528 .detector_bundles
529 .iter()
530 .filter_map(|&index| detector_bundles[index as usize])
531 .collect();
532 guard.restarts = guard
533 .restarts
534 .iter()
535 .filter_map(|&index| restarts[index as usize])
536 .collect();
537 guard.detector_parities = guard
538 .detector_parities
539 .iter()
540 .filter_map(|(index, parity)| {
541 detectors[*index as usize].map(|index| (index, parity.clone()))
542 })
543 .collect();
544 guard.restart_parities = guard
545 .restart_parities
546 .iter()
547 .filter_map(|(index, parity)| {
548 restarts[*index as usize].map(|index| (index, parity.clone()))
549 })
550 .collect();
551 }
552 pending.retain(|guard| {
553 !guard.instances.is_empty()
554 || !guard.detectors.is_empty()
555 || !guard.detector_bundles.is_empty()
556 || !guard.restarts.is_empty()
557 || !guard.detector_parities.is_empty()
558 || !guard.restart_parities.is_empty()
559 });
560 let mut selected = node.clone();
561 selected
562 .expect_quantum_mut()
563 .guards
564 .retain(|guard| inputs.contains_key(&guard.input));
565 let mut selected = selected.select_quantum_members(|slot| inputs.get(&slot).copied())?;
566 selected.expect_quantum_mut().guards = pending;
567 Ok(selected)
568}
569
570fn simplified(expr: &ClassicalExpr, values: &FxMap<u32, Option<bool>>) -> ClassicalExpr {
571 use ClassicalExpr::{And, Const, In, Not, Or, Select, Xor};
572 match expr {
573 Const(_) => expr.clone(),
574 ClassicalExpr::Parity { inputs, constant } => {
575 let mut constant = *constant;
576 let inputs = inputs
577 .iter()
578 .copied()
579 .filter(|slot| {
580 if let Some(Some(value)) = values.get(slot) {
581 constant ^= value;
582 false
583 } else {
584 true
585 }
586 })
587 .collect::<Vec<_>>();
588 ClassicalExpr::parity(inputs, constant)
589 }
590 In(slot) => values
591 .get(slot)
592 .copied()
593 .flatten()
594 .map(Const)
595 .unwrap_or_else(|| expr.clone()),
596 Not(inner) => match simplified(inner, values) {
597 Const(value) => Const(!value),
598 Not(inner) => *inner,
599 inner => Not(Box::new(inner)),
600 },
601 And(operands) | Or(operands) | Xor(operands) => {
602 let mut constant = matches!(expr, And(_));
603 let mut remaining = Vec::with_capacity(operands.len());
604 for operand in operands {
605 match simplified(operand, values) {
606 Const(bit) => {
607 match expr {
608 And(_) => constant &= bit,
609 Or(_) => constant |= bit,
610 _ => constant ^= bit,
611 }
612 if matches!(expr, And(_)) && !constant || matches!(expr, Or(_)) && constant
613 {
614 return Const(constant);
615 }
616 }
617 operand => remaining.push(operand),
618 }
619 }
620 if remaining.is_empty() {
621 return Const(constant);
622 }
623 let value = if remaining.len() == 1 {
624 remaining
625 .pop()
626 .expect("the single remaining operand was counted above")
627 } else {
628 match expr {
629 And(_) => And(remaining.into_boxed_slice()),
630 Or(_) => Or(remaining.into_boxed_slice()),
631 _ => Xor(remaining.into_boxed_slice()),
632 }
633 };
634 if matches!(expr, Xor(_)) && constant {
635 match value {
636 Not(inner) => *inner,
637 value => Not(Box::new(value)),
638 }
639 } else {
640 value
641 }
642 }
643 Select(operands) => {
644 let [condition, when_false, when_true] = operands.as_ref();
645 let condition = simplified(condition, values);
646 if let Const(bit) = condition {
648 return simplified(if bit { when_true } else { when_false }, values);
649 }
650 let when_false = simplified(when_false, values);
651 let when_true = simplified(when_true, values);
652 if when_false == when_true {
653 when_false
654 } else {
655 ClassicalExpr::select(condition, when_false, when_true)
656 }
657 }
658 }
659}
660
661fn prune_unused_values(level: &mut SubGraph) {
662 let mut roots = level
663 .edges()
664 .filter_map(|edge| match edge.edge {
665 BloqEdge::Quantum(quantum) => quantum.guard.map(|value| value.node),
666 _ => None,
667 })
668 .collect::<FxSet<_>>();
669 roots.extend(level.value_output().map(|value| value.node));
670 roots.extend(level.boundary_outputs());
671 for id in level
672 .deterministic_emit_order()
673 .expect("pinned graph is acyclic")
674 .into_iter()
675 .rev()
676 {
677 let node = &level[id];
678 if roots.contains(&id)
679 || matches!(
680 node.provenance,
681 NodeProvenance::OutputFrame { .. } | NodeProvenance::BranchSelector { .. }
682 )
683 {
684 continue;
685 }
686 if matches!(
687 node.try_classical(),
688 Some(ClassicalNode::Compute { .. } | ClassicalNode::Observable { index: None, .. })
689 ) && level.data_consumers(id).next().is_none()
690 {
691 level.remove_node(id);
692 }
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use crate::BloqValidationError;
699
700 use super::*;
701
702 #[test]
703 fn pinning_an_observable_corrected_port_preserves_flip_and_recipe_consumers() {
704 let program = Bloq::from_text(
705 "BLOQIR 1
706 graph {
707 n0 observable 0 from selector choice
708 n1 compute in0 from frame x (0,0,1)
709 n2 compute in0 from frame z (0,0,1)
710 n3 observable 1
711 n0 -> n1 value 0
712 n0 -> n2 value 0 flip
713 n0 -> n3 compose 0
714 result n0:flip
715 }",
716 )
717 .unwrap();
718 for bit in [false, true] {
719 let pinned = program
720 .pin_membership(&BTreeMap::from([("choice".to_owned(), bit)]))
721 .unwrap();
722 pinned.validate().unwrap();
723 assert!(matches!(
724 pinned[BloqNodeId(0)].try_classical(),
725 Some(ClassicalNode::Observable { index: Some(0), .. })
726 ));
727 assert_eq!(
728 pinned.classical_value(BloqNodeId(1), crate::ClassicalAssignment::Uniform(!bit)),
729 Ok(bit)
730 );
731 assert_eq!(
732 pinned.top().value_output(),
733 Some(ValueRef {
734 node: BloqNodeId(0),
735 output: ObservableOutput::Flip
736 })
737 );
738 assert_eq!(
739 pinned
740 .resolve_classical(BloqNodeId(2), crate::ClassicalAssignment::Uniform(false))
741 .unwrap()
742 .decoder_observables,
743 std::collections::BTreeSet::from([0])
744 );
745 assert_eq!(
746 pinned
747 .resolve_classical(BloqNodeId(3), crate::ClassicalAssignment::Uniform(false))
748 .unwrap()
749 .decoder_observables,
750 std::collections::BTreeSet::from([1])
751 );
752 }
753 }
754
755 #[test]
756 fn pinning_keeps_an_inactive_indexed_observables_ports_false() {
757 let program = Bloq::from_text(
758 "BLOQIR 1
759 template t0 {
760 circuit {
761 M (0,0):m0
762 }
763 }
764 graph {
765 n0 compute 0
766 n1 observable 0 measurements i0:m0 when v0
767 n2 compute in0
768 n3 quantum {
769 instance i0 t0 @ (0,0)
770 guard 0 i0
771 }
772 n0 -> n3 value 0
773 n3 -> n1 order
774 n1 -> n2 value 0 flip
775 n0 -> n1 value 0
776 result n1:flip
777 }",
778 )
779 .unwrap();
780 let pinned = program.pin_membership(&BTreeMap::new()).unwrap();
781 pinned.validate().unwrap();
782 assert_eq!(
783 pinned.top().value_output(),
784 Some(ValueRef {
785 node: BloqNodeId(1),
786 output: ObservableOutput::Flip
787 })
788 );
789 for output in [ObservableOutput::Corrected, ObservableOutput::Flip] {
790 let value = ValueRef {
791 node: BloqNodeId(1),
792 output,
793 };
794 assert_eq!(
795 pinned.classical_ref_value(value, crate::ClassicalAssignment::Uniform(true)),
796 Ok(false)
797 );
798 let resolution = pinned
799 .resolve_value(value, crate::ClassicalAssignment::Uniform(true))
800 .unwrap();
801 assert!(resolution.measurements.is_empty());
802 assert!(resolution.decoder_observables.is_empty());
803 }
804 }
805
806 #[test]
807 fn cyclic_pinning_retains_the_causal_error() {
808 let mut program = Bloq::new();
809 let node = program.add_node(crate::BloqNode::classical(ClassicalNode::Compute {
810 expr: ClassicalExpr::Const(true),
811 }));
812 program.add_edge(node, node, BloqEdge::Order);
813
814 let error = program.pin_membership(&BTreeMap::new()).unwrap_err();
815 assert!(matches!(error, MembershipPinError::Cycle(CycleDetected)));
816 assert!(
817 std::error::Error::source(&error)
818 .unwrap()
819 .is::<CycleDetected>()
820 );
821 }
822
823 #[test]
824 fn partial_pin_renumbers_pending_bundle_only_registration() {
825 use crate::{BloqNode, DetectorBundleId, DetectorBundleUse, QuantumGuard};
826 let mut node = BloqNode::from_members(Vec::new());
827 node.expect_quantum_mut().detector_bundles = (0..2)
828 .map(|index| DetectorBundleUse {
829 bundle: DetectorBundleId(index),
830 instances: Vec::new(),
831 offset: glam::IVec2::ZERO,
832 })
833 .collect();
834 node.expect_quantum_mut().guards = vec![
835 QuantumGuard {
836 input: 0,
837 detector_bundles: vec![0],
838 ..Default::default()
839 },
840 QuantumGuard {
841 input: 1,
842 detector_bundles: vec![1],
843 ..Default::default()
844 },
845 ];
846 let pinned = pin_quantum_members(&node, &FxMap::from_iter([(0, false)])).unwrap();
847 let quantum = pinned.expect_quantum();
848 assert_eq!(quantum.detector_bundles[0].bundle, DetectorBundleId(1));
849 assert_eq!(quantum.guards[0].detector_bundles, vec![0]);
850 assert_eq!(quantum.guards[0].input, 1);
851 }
852
853 #[test]
854 fn pinning_does_not_audit_unrelated_observable_indices() {
855 let mut program = Bloq::new();
856 for _ in 0..2 {
857 program.add_node(crate::BloqNode::classical(ClassicalNode::observable(0)));
858 }
859 assert!(matches!(
860 program.validate(),
861 Err(BloqValidationError::DuplicateObservableIndex { .. })
862 ));
863 let pinned = program.pin_membership(&BTreeMap::new()).unwrap();
864 assert_eq!(pinned.nodes().count(), 2);
865 }
866
867 #[test]
868 fn missing_activation_input_is_a_pinning_error() {
869 let mut program = Bloq::new();
870 let id = program.add_node(crate::BloqNode::classical(ClassicalNode::Compute {
871 expr: ClassicalExpr::Const(true),
872 }));
873 program.node_mut(id).unwrap().activation = Some(0);
874 assert!(matches!(
875 program.pin_membership(&BTreeMap::new()),
876 Err(MembershipPinError::InvalidMembership(
877 NodeTemplateInstanceMergeError::MissingMembershipInput(0)
878 ))
879 ));
880 }
881
882 #[test]
883 fn compound_alias_pins_fix_shared_guards_without_guessing_other_outcomes() {
884 let program = Bloq::from_text(
885 "BLOQIR 1
886template t0 {
887 circuit {
888 M (0,0):m0
889 M (1,0):m1
890 M (2,0):m2
891 }
892}
893template t1 {
894 circuit {
895 M (0,0):m0
896 }
897}
898graph {
899 n0 quantum {
900 instance i0 t0 @ (0,0)
901 }
902 n1 observable fragment measurements i0:m0
903 n2 observable fragment measurements i0:m1
904 n3 observable fragment measurements i0:m2
905 n4 compute in0 & in1
906 n5 compute in0 from selector both
907 n6 compute !in0 from selector not_both
908 n7 compute !(in0 & in1)
909 n8 compute in0 & in1 & in2
910 n9 quantum {
911 instance i1 t1 @ (3,0)
912 instance i2 t1 @ (3,0)
913 guard 0 i1
914 guard 1 i2
915 }
916 n10 quantum {
917 instance i3 t1 @ (4,0)
918 guard 0 i3
919 }
920 n11 observable fragment measurements i1:m0 when v0
921 n12 observable fragment measurements i2:m0 when v0
922 n13 observable 0
923 n14 compute in0 ^ in1
924 n15 compute 0 when v0
925 n16 compute in0 & in1
926 n0 -> n1 order
927 n0 -> n2 order
928 n0 -> n3 order
929 n1 -> n4 value 0
930 n2 -> n4 value 1
931 n4 -> n5 value 0
932 n4 -> n6 value 0
933 n1 -> n7 value 0
934 n2 -> n7 value 1
935 n1 -> n8 value 0
936 n2 -> n8 value 1
937 n3 -> n8 value 2
938 n4 -> n9 value 0
939 n7 -> n9 value 1
940 n8 -> n10 value 0
941 n4 -> n11 value 0
942 n7 -> n12 value 0
943 n9 -> n11 order
944 n9 -> n12 order
945 n11 -> n13 compose 0
946 n12 -> n13 compose 1
947 n3 -> n14 value 0
948 n3 -> n14 value 1
949 n3 -> n15 value 0
950 n15 -> n13 value 2
951 n1 -> n16 value 0
952 n2 -> n16 value 1
953 n16 -> n13 value 3
954 result n14
955}",
956 )
957 .unwrap();
958 let original = program.to_binary();
959 for both in [false, true] {
960 let pinned = program
961 .pin_membership(&BTreeMap::from([
962 ("both".to_owned(), both),
963 ("not_both".to_owned(), !both),
964 ]))
965 .unwrap();
966 let selected = pinned[BloqNodeId(9)].expect_quantum();
967 assert!(selected.guards.is_empty());
968 assert_eq!(selected.instances.len(), 1);
969 assert_eq!(selected.instances[0].id.0, if both { 1 } else { 2 });
970 let read = BloqNodeId(if both { 11 } else { 12 });
971 assert_eq!(pinned[read].try_classical(), program[read].try_classical());
972 assert_eq!(pinned[read].activation, None);
973 let masked = pinned[BloqNodeId(10)].expect_quantum();
974 assert_eq!(masked.instances.len(), usize::from(both));
975 assert_eq!(masked.guards.len(), usize::from(both));
976 assert_eq!(
977 pinned[BloqNodeId(3)].try_classical(),
978 program[BloqNodeId(3)].try_classical()
979 );
980 assert_eq!(
981 pinned[BloqNodeId(14)].try_classical(),
982 program[BloqNodeId(14)].try_classical()
983 );
984 assert_eq!(pinned.value_inputs(BloqNodeId(14)).count(), 2);
985 assert_eq!(pinned[BloqNodeId(15)].activation, Some(0));
986 assert_eq!(
987 pinned[BloqNodeId(16)].try_classical(),
988 program[BloqNodeId(16)].try_classical(),
989 "unrelated readout recipes retain their measured inputs"
990 );
991 let Some(ClassicalNode::Compute { expr }) = pinned[BloqNodeId(14)].try_classical()
992 else {
993 unreachable!()
994 };
995 assert_eq!(expr.eval(&mut |_| None), None);
996 }
997 assert!(matches!(
998 program.pin_membership(&BTreeMap::from([
999 ("both".to_owned(), true),
1000 ("not_both".to_owned(), true),
1001 ])),
1002 Err(MembershipPinError::UnreachableAssignment)
1003 ));
1004 assert_eq!(program.to_binary(), original);
1005 }
1006
1007 #[test]
1008 fn pinning_activated_selector_keeps_its_value_and_shares_unchanged_quantum_data() {
1009 let program = Bloq::from_text(
1010 "BLOQIR 1
1011template t0 {
1012 circuit {
1013 M (0,0):m0
1014 }
1015}
1016graph {
1017 n0 quantum {
1018 instance i0 t0 @ (0,0)
1019 }
1020 n1 observable fragment measurements i0:m0
1021 n2 compute 1 when v0 from selector choice
1022 n3 quantum {
1023 instance i1 t0 @ (1,0)
1024 guard 0 i1
1025 }
1026 n4 observable fragment measurements i1:m0 when v0
1027 n0 -> n1 order
1028 n1 -> n2 value 0
1029 n2 -> n3 value 0
1030 n2 -> n4 value 0
1031 n3 -> n4 order
1032 result n4
1033}",
1034 )
1035 .unwrap();
1036 let original = program.to_binary();
1037 for bit in [false, true] {
1038 let pinned = program
1039 .pin_membership(&BTreeMap::from([("choice".to_owned(), bit)]))
1040 .unwrap();
1041 assert_eq!(pinned[BloqNodeId(2)].activation, None);
1042 assert!(pinned.value_inputs(BloqNodeId(2)).next().is_none());
1043 assert_eq!(
1044 pinned[BloqNodeId(2)].try_classical(),
1045 Some(&ClassicalNode::Compute {
1046 expr: ClassicalExpr::Const(bit),
1047 })
1048 );
1049 assert!(std::ptr::eq(
1050 program[BloqNodeId(0)].expect_quantum(),
1051 pinned[BloqNodeId(0)].expect_quantum(),
1052 ));
1053 assert!(!pinned.has_conditional_membership());
1054 assert_eq!(
1055 pinned[BloqNodeId(3)].expect_quantum().instances.len(),
1056 usize::from(bit),
1057 );
1058 assert_eq!(pinned[BloqNodeId(4)].activation, None);
1059 if bit {
1060 assert!(matches!(
1061 pinned[BloqNodeId(4)].try_classical(),
1062 Some(ClassicalNode::Observable { index: None, .. })
1063 ));
1064 } else {
1065 assert_eq!(
1066 pinned[BloqNodeId(4)].try_classical(),
1067 Some(&ClassicalNode::Observable {
1068 index: None,
1069 measurements: Vec::new(),
1070 operators: Vec::new(),
1071 })
1072 );
1073 }
1074 }
1075 assert_eq!(program.to_binary(), original);
1076
1077 let mut inactive = program.clone();
1078 inactive.node_mut(BloqNodeId(1)).unwrap().kind = BloqNodeKind::Classical(
1079 ClassicalNode::Compute {
1080 expr: ClassicalExpr::Const(false),
1081 }
1082 .into(),
1083 );
1084 inactive
1085 .pin_membership(&BTreeMap::from([("choice".to_owned(), false)]))
1086 .unwrap();
1087 assert!(matches!(
1088 inactive.pin_membership(&BTreeMap::from([("choice".to_owned(), true)])),
1089 Err(MembershipPinError::UnreachableAssignment)
1090 ));
1091 }
1092
1093 #[test]
1094 fn disabled_region_keeps_false_result_and_clears_boundary_exports() {
1095 let program = Bloq::from_text(
1096 "BLOQIR 1
1097graph {
1098 n0 compute 0
1099 n1 rus 0 when v0 {
1100 body {
1101 n0 observable fragment
1102 bindings n0
1103 }
1104 }
1105 n0 -> n1 value 0
1106 result n1
1107 bindings n1
1108}",
1109 )
1110 .unwrap();
1111 let pinned = program.pin_membership(&BTreeMap::new()).unwrap();
1112 assert_eq!(pinned.value_output(), Some(BloqNodeId(1).into()));
1113 assert!(pinned.boundary_outputs().is_empty());
1114 assert!(matches!(
1115 pinned[BloqNodeId(1)].try_classical(),
1116 Some(ClassicalNode::Compute {
1117 expr: ClassicalExpr::Const(false)
1118 })
1119 ));
1120 assert_eq!(program.boundary_outputs(), [BloqNodeId(1)]);
1121 }
1122
1123 #[test]
1124 fn expression_cofactors_preserve_every_completion_of_partial_pins() {
1125 use ClassicalExpr::{And, Const, In, Not, Or, Xor};
1126 for expr in [
1127 ClassicalExpr::parity([0, 1, 2], true),
1128 ClassicalExpr::parity([0, 0, 2], false),
1129 Xor(Box::new([In(0), In(1), In(2), Const(true)])),
1130 And(Box::new([In(0), In(1), In(2)])),
1131 Or(Box::new([In(0), In(1), In(2)])),
1132 ClassicalExpr::select(In(0), In(1), In(2)),
1133 ClassicalExpr::select(
1134 Not(Box::new(In(0))),
1135 Xor(Box::new([In(1), In(2)])),
1136 And(Box::new([In(1), In(2)])),
1137 ),
1138 ] {
1139 for encoded in 0..27 {
1140 let values = (0..3)
1141 .map(|slot| {
1142 (
1143 slot,
1144 [None, Some(false), Some(true)][encoded / 3usize.pow(slot) % 3],
1145 )
1146 })
1147 .collect::<FxMap<_, _>>();
1148 let cofactor = simplified(&expr, &values);
1149 for bits in 0..8 {
1150 if values.iter().any(|(&slot, value)| {
1151 value.is_some_and(|value| value != (bits & (1 << slot) != 0))
1152 }) {
1153 continue;
1154 }
1155 assert_eq!(
1156 cofactor.eval(&mut |slot| Some(bits & (1 << slot) != 0)),
1157 expr.eval(&mut |slot| Some(bits & (1 << slot) != 0)),
1158 "{expr:?}, {values:?}, {bits}"
1159 );
1160 }
1161 if values.values().all(Option::is_some) {
1162 assert!(
1163 matches!(cofactor, Const(_)),
1164 "fully pinned expression must become a constant"
1165 );
1166 }
1167 }
1168 }
1169 }
1170}