1use std::collections::{BTreeMap, BTreeSet};
24
25use thiserror::Error;
26
27use crate::{
28 Bloq, BloqNodeId, BloqNodeKind, ClassicalExpr, ClassicalNode, InstanceMeasurement,
29 NodeDetectorParity, ObservableOutput, RegionNode, SubGraph, ValueRef,
30};
31
32#[derive(Debug, Clone, Copy)]
36pub enum ClassicalAssignment<'a> {
37 Uniform(bool),
39 Pinned {
42 forced_observables: &'a BTreeMap<u32, bool>,
46 },
47}
48
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
51enum FoldMode {
52 #[default]
53 Parity,
54 MeasurementDependencies,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ClassicalResolution {
63 pub measurements: BTreeSet<InstanceMeasurement>,
65 pub sign: bool,
67 pub decoder_observables: BTreeSet<u32>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Error)]
74#[non_exhaustive]
75pub enum ResolveError {
76 #[error("node {node:?} does not exist at the level being resolved")]
78 MissingNode {
79 node: BloqNodeId,
81 },
82 #[error("classical fold reached quantum node {node:?}")]
84 NotClassical {
85 node: BloqNodeId,
87 },
88 #[error("classical node {node:?} has no value a static fold can produce")]
90 UnsupportedNode {
91 node: BloqNodeId,
93 },
94 #[error("node {node:?} reads slot {slot}, which has no producer")]
96 UnwiredSlot {
97 node: BloqNodeId,
99 slot: u32,
101 },
102 #[error("node {node:?} folds a non-linear expression, which has no measurement recipe")]
104 NonLinearExpr {
105 node: BloqNodeId,
107 },
108 #[error("condition at node {node:?} is not fixed by the resolved path")]
110 ConditionUnresolved {
111 node: BloqNodeId,
113 },
114 #[error("value graph cycles through node {node:?}")]
116 CyclicValueGraph {
117 node: BloqNodeId,
119 },
120}
121
122#[derive(Debug, Clone, Default, PartialEq, Eq)]
124struct Value {
125 parity: NodeDetectorParity,
126 decoder_observables: BTreeSet<u32>,
127}
128
129impl Value {
130 fn from_measurements(measurements: impl IntoIterator<Item = InstanceMeasurement>) -> Self {
131 Self {
132 parity: NodeDetectorParity::from_measurements(measurements),
133 decoder_observables: BTreeSet::new(),
134 }
135 }
136
137 fn measurements(&self) -> impl Iterator<Item = InstanceMeasurement> + '_ {
138 self.parity.measurements()
139 }
140
141 fn sign(&self) -> bool {
142 self.parity.sign()
143 }
144
145 fn with_sign(mut self, sign: bool) -> Self {
146 self.parity = self.parity.with_sign(sign);
147 self
148 }
149
150 fn xor_assign(&mut self, other: &Self) {
151 self.parity.xor_assign(&other.parity);
152 for &observable in &other.decoder_observables {
153 if !self.decoder_observables.insert(observable) {
154 self.decoder_observables.remove(&observable);
155 }
156 }
157 }
158
159 fn with_decoder_observable(mut self, observable: u32) -> Self {
160 if !self.decoder_observables.insert(observable) {
161 self.decoder_observables.remove(&observable);
162 }
163 self
164 }
165}
166
167fn value_inputs_by_slot(level: &SubGraph, node: BloqNodeId) -> BTreeMap<u32, ValueRef> {
172 level
173 .value_inputs(node)
174 .map(|input| (input.slot, input.value_ref().expect("runtime input")))
175 .collect()
176}
177
178fn check_output(
179 level: &SubGraph,
180 node: BloqNodeId,
181 output: Option<ObservableOutput>,
182) -> Result<(), ResolveError> {
183 if output == Some(ObservableOutput::Flip) {
184 let payload = level.node(node).ok_or(ResolveError::MissingNode { node })?;
185 if payload.try_quantum().is_some() {
186 return Err(ResolveError::NotClassical { node });
187 }
188 if !matches!(
189 payload.try_classical(),
190 Some(ClassicalNode::Observable { index: Some(_), .. })
191 ) {
192 return Err(ResolveError::UnsupportedNode { node });
193 }
194 }
195 Ok(())
196}
197
198impl SubGraph {
199 pub fn resolve_classical(
208 &self,
209 node: BloqNodeId,
210 assignment: ClassicalAssignment<'_>,
211 ) -> Result<ClassicalResolution, ResolveError> {
212 self.resolve_value(node.into(), assignment)
213 }
214
215 pub fn resolve_value(
220 &self,
221 value: ValueRef,
222 assignment: ClassicalAssignment<'_>,
223 ) -> Result<ClassicalResolution, ResolveError> {
224 let mut resolver = Resolver::new(assignment, FoldMode::Parity);
225 let value = resolver.node_value(self, value)?;
226 Ok(ClassicalResolution {
227 measurements: value.measurements().collect(),
228 sign: value.sign(),
229 decoder_observables: value.decoder_observables,
230 })
231 }
232}
233
234impl Bloq {
235 pub fn resolve_classical(
251 &self,
252 node: BloqNodeId,
253 assignment: ClassicalAssignment<'_>,
254 ) -> Result<ClassicalResolution, ResolveError> {
255 self.top().resolve_classical(node, assignment)
256 }
257
258 pub fn resolve_value(
263 &self,
264 value: ValueRef,
265 assignment: ClassicalAssignment<'_>,
266 ) -> Result<ClassicalResolution, ResolveError> {
267 self.top().resolve_value(value, assignment)
268 }
269
270 pub fn measurement_dependencies(
279 &self,
280 node: BloqNodeId,
281 assignment: ClassicalAssignment<'_>,
282 ) -> Result<BTreeSet<InstanceMeasurement>, ResolveError> {
283 self.measurement_value_dependencies(node.into(), assignment)
284 }
285
286 pub fn measurement_value_dependencies(
291 &self,
292 value: ValueRef,
293 assignment: ClassicalAssignment<'_>,
294 ) -> Result<BTreeSet<InstanceMeasurement>, ResolveError> {
295 let mut resolver = Resolver::new(assignment, FoldMode::MeasurementDependencies);
296 Ok(resolver
297 .node_value(self.top(), value)?
298 .measurements()
299 .collect())
300 }
301
302 pub fn classical_value(
322 &self,
323 node: BloqNodeId,
324 assignment: ClassicalAssignment<'_>,
325 ) -> Result<bool, ResolveError> {
326 self.classical_ref_value(node.into(), assignment)
327 }
328
329 pub fn classical_ref_value(
334 &self,
335 value: ValueRef,
336 assignment: ClassicalAssignment<'_>,
337 ) -> Result<bool, ResolveError> {
338 let node = value.node;
339 let mut resolver = Resolver::new(assignment, FoldMode::Parity);
340 let level = self.top();
341 match &level
342 .node(node)
343 .ok_or(ResolveError::MissingNode { node })?
344 .kind
345 {
346 BloqNodeKind::Region(_) => Err(ResolveError::UnsupportedNode { node }),
347 BloqNodeKind::Quantum(_) => Err(ResolveError::NotClassical { node }),
348 BloqNodeKind::Classical(_) => resolver.condition_producer(level, value),
349 }
350 }
351}
352
353struct Resolver<'a> {
354 assignment: ClassicalAssignment<'a>,
355 mode: FoldMode,
356 active: BTreeSet<(*const SubGraph, BloqNodeId, Option<ObservableOutput>)>,
362 values: BTreeMap<
363 (*const SubGraph, BloqNodeId, Option<ObservableOutput>),
364 Result<Value, ResolveError>,
365 >,
366 conditions: BTreeMap<(*const SubGraph, ValueRef), Result<bool, ResolveError>>,
367}
368
369impl<'a> Resolver<'a> {
370 fn new(assignment: ClassicalAssignment<'a>, mode: FoldMode) -> Self {
371 Self {
372 assignment,
373 mode,
374 active: BTreeSet::new(),
375 values: BTreeMap::new(),
376 conditions: BTreeMap::new(),
377 }
378 }
379 fn combine(&self, mut left: Value, right: &Value) -> Value {
380 if self.mode != FoldMode::Parity {
381 let mut measurements: BTreeSet<_> = left.measurements().collect();
382 measurements.extend(right.measurements());
383 Value::from_measurements(measurements)
384 } else {
385 left.xor_assign(right);
386 left
387 }
388 }
389
390 fn node_value(
391 &mut self,
392 level: &SubGraph,
393 value: impl Into<ValueRef>,
394 ) -> Result<Value, ResolveError> {
395 let value = value.into();
396 self.node_value_at(level, value.node, Some(value.output))
397 }
398
399 fn node_value_at(
400 &mut self,
401 level: &SubGraph,
402 node: BloqNodeId,
403 output: Option<ObservableOutput>,
404 ) -> Result<Value, ResolveError> {
405 let frame = (std::ptr::from_ref(level), node, output);
406 if let Some(value) = self.values.get(&frame) {
407 return value.clone();
408 }
409 if !self.active.insert(frame) {
410 return Err(ResolveError::CyclicValueGraph { node });
411 }
412 let value = (|| {
413 check_output(level, node, output)?;
414 if !self.node_active(level, node)? {
415 return Ok(Value::default());
416 }
417 self.node_value_uncycled(level, node, output)
418 })();
419 self.active.remove(&frame);
420 self.values.insert(frame, value.clone());
421 value
422 }
423
424 fn node_active(&mut self, level: &SubGraph, node: BloqNodeId) -> Result<bool, ResolveError> {
425 let activation = level
426 .node(node)
427 .ok_or(ResolveError::MissingNode { node })?
428 .activation;
429 let Some(slot) = activation else {
430 return Ok(true);
431 };
432 let guard = level
433 .value_inputs(node)
434 .find(|input| input.slot == slot)
435 .map(|input| input.value_ref().expect("runtime input"))
436 .ok_or(ResolveError::UnwiredSlot { node, slot })?;
437 self.condition_producer(level, guard)
438 }
439
440 fn measurement_value(&self, measurements: &[InstanceMeasurement]) -> Value {
441 if self.mode == FoldMode::Parity {
442 Value::from_measurements(measurements.iter().copied())
443 } else {
444 Value::from_measurements(measurements.iter().copied().collect::<BTreeSet<_>>())
445 }
446 }
447
448 fn node_value_uncycled(
449 &mut self,
450 level: &SubGraph,
451 node: BloqNodeId,
452 output: Option<ObservableOutput>,
453 ) -> Result<Value, ResolveError> {
454 let payload = match &level
455 .node(node)
456 .ok_or(ResolveError::MissingNode { node })?
457 .kind
458 {
459 BloqNodeKind::Region(region) => {
460 return self.region_value(region);
461 }
462 BloqNodeKind::Quantum(_) => return Err(ResolveError::NotClassical { node }),
463 BloqNodeKind::Classical(payload) => payload.as_ref(),
464 };
465 match payload {
466 ClassicalNode::Compute { expr } => {
467 let inputs = value_inputs_by_slot(level, node);
468 self.expression(level, node, expr, &inputs)
469 }
470 ClassicalNode::Observable {
471 index,
472 measurements,
473 ..
474 } => {
475 let mut value = self.measurement_value(measurements);
476 for input in level.data_inputs(node) {
477 let input = self.node_value_at(level, input.producer, input.output)?;
478 value = self.combine(value, &input);
479 }
480 if self.mode == FoldMode::Parity {
481 match (output, index) {
482 (Some(ObservableOutput::Corrected), Some(index)) => {
483 value = value.with_decoder_observable(*index)
484 }
485 (Some(ObservableOutput::Flip), Some(index)) => {
486 value = Value::default().with_decoder_observable(*index)
487 }
488 _ => {}
489 }
490 }
491 Ok(value)
492 }
493 ClassicalNode::Discard { .. } => Err(ResolveError::UnsupportedNode { node }),
494 }
495 }
496
497 fn region_value(&mut self, region: &RegionNode) -> Result<Value, ResolveError> {
500 let RegionNode::RepeatUntilSuccess { body, .. } = region;
502 self.body_value(body)
503 }
504
505 fn body_value(&mut self, body: &SubGraph) -> Result<Value, ResolveError> {
506 body.value_output().map_or_else(
507 || Ok(Value::default()),
508 |producer| self.node_value(body, producer),
509 )
510 }
511
512 fn expression(
513 &mut self,
514 level: &SubGraph,
515 node: BloqNodeId,
516 expr: &ClassicalExpr,
517 inputs: &BTreeMap<u32, ValueRef>,
518 ) -> Result<Value, ResolveError> {
519 match expr {
520 ClassicalExpr::In(slot) => {
521 let producer = *inputs
522 .get(slot)
523 .ok_or(ResolveError::UnwiredSlot { node, slot: *slot })?;
524 self.node_value(level, producer)
525 }
526 ClassicalExpr::Const(bit) => Ok(Value::default().with_sign(*bit)),
527 ClassicalExpr::Parity {
528 inputs: slots,
529 constant,
530 } => {
531 let mut value = Value::default().with_sign(*constant);
532 for &slot in slots {
533 let operand = self.expression(level, node, &ClassicalExpr::In(slot), inputs)?;
534 value = self.combine(value, &operand);
535 }
536 Ok(value)
537 }
538 ClassicalExpr::Not(inner) => {
539 let value = self.expression(level, node, inner, inputs)?;
540 let sign = !value.sign();
541 Ok(value.with_sign(sign))
542 }
543 ClassicalExpr::Select(operands) if self.mode == FoldMode::Parity => {
544 let [condition, when_false, when_true] = operands.as_ref();
545 let selected = if self.fixed_condition(level, node, condition)? {
546 when_true
547 } else {
548 when_false
549 };
550 self.expression(level, node, selected, inputs)
551 }
552 ClassicalExpr::And(..) | ClassicalExpr::Or(..) if self.mode == FoldMode::Parity => {
553 let value = self.fixed_condition(level, node, expr)?;
554 Ok(Value::default().with_sign(value))
555 }
556 ClassicalExpr::Xor(_)
557 | ClassicalExpr::And(_)
558 | ClassicalExpr::Or(_)
559 | ClassicalExpr::Select(_) => {
560 let mut value = Value::default();
561 for operand in expr.operands() {
562 let operand = self.expression(level, node, operand, inputs)?;
563 value = self.combine(value, &operand);
564 }
565 Ok(value)
566 }
567 }
568 }
569
570 fn condition(
575 &mut self,
576 level: &SubGraph,
577 node: BloqNodeId,
578 expr: &ClassicalExpr,
579 ) -> Result<bool, ResolveError> {
580 match expr {
581 ClassicalExpr::Const(bit) => Ok(*bit),
582 ClassicalExpr::Parity { inputs, constant } => {
583 inputs.iter().try_fold(*constant, |value, &slot| {
584 Ok(value ^ self.condition(level, node, &ClassicalExpr::In(slot))?)
585 })
586 }
587 ClassicalExpr::In(slot) => match self.assignment {
588 ClassicalAssignment::Uniform(pin) => Ok(pin),
591 ClassicalAssignment::Pinned { .. } => {
592 let producer = level
593 .value_inputs(node)
594 .find(|input| input.slot == *slot)
595 .map(|input| input.value_ref().expect("runtime input"))
596 .ok_or(ResolveError::UnwiredSlot { node, slot: *slot })?;
597 self.condition_producer(level, producer)
598 }
599 },
600 ClassicalExpr::Not(inner) => Ok(!self.condition(level, node, inner)?),
601 ClassicalExpr::Xor(operands) => operands.iter().try_fold(false, |value, operand| {
602 Ok(value ^ self.condition(level, node, operand)?)
603 }),
604 ClassicalExpr::And(operands) => operands.iter().try_fold(true, |value, operand| {
605 Ok(value & self.condition(level, node, operand)?)
606 }),
607 ClassicalExpr::Or(operands) => operands.iter().try_fold(false, |value, operand| {
608 Ok(value | self.condition(level, node, operand)?)
609 }),
610 ClassicalExpr::Select(operands) => {
611 let [condition, when_false, when_true] = operands.as_ref();
612 let condition = self.condition(level, node, condition)?;
613 let when_false = self.condition(level, node, when_false)?;
614 let when_true = self.condition(level, node, when_true)?;
615 Ok(if condition { when_true } else { when_false })
616 }
617 }
618 }
619
620 fn fixed_condition(
624 &mut self,
625 level: &SubGraph,
626 node: BloqNodeId,
627 expr: &ClassicalExpr,
628 ) -> Result<bool, ResolveError> {
629 self.condition(level, node, expr)
630 .map_err(|error| match error {
631 ResolveError::ConditionUnresolved { .. } => ResolveError::NonLinearExpr { node },
632 error => error,
633 })
634 }
635
636 fn condition_producer(
643 &mut self,
644 level: &SubGraph,
645 value: impl Into<ValueRef>,
646 ) -> Result<bool, ResolveError> {
647 let value = value.into();
648 let node = value.node;
649 let frame = (std::ptr::from_ref(level), value);
650 if let Some(value) = self.conditions.get(&frame) {
651 return value.clone();
652 }
653 if !self.active.insert((frame.0, node, Some(value.output))) {
654 return Err(ResolveError::CyclicValueGraph { node });
655 }
656 let result = self.condition_producer_uncycled(level, value);
657 self.active.remove(&(frame.0, node, Some(value.output)));
658 self.conditions.insert(frame, result.clone());
659 result
660 }
661
662 fn condition_producer_uncycled(
663 &mut self,
664 level: &SubGraph,
665 value: ValueRef,
666 ) -> Result<bool, ResolveError> {
667 let node = value.node;
668 check_output(level, node, Some(value.output))?;
669 let payload = level.node(node).ok_or(ResolveError::MissingNode { node })?;
670 if let Some(slot) = payload.activation {
671 let guard = level
672 .value_inputs(node)
673 .find(|input| input.slot == slot)
674 .ok_or(ResolveError::UnwiredSlot { node, slot })?;
675 let guard = guard.value_ref().expect("runtime input");
676 if !self.condition_producer(level, guard)? {
677 return Ok(false);
678 }
679 }
680 let classical = payload
681 .try_classical()
682 .ok_or(ResolveError::NotClassical { node })?;
683 let observable = match classical {
684 ClassicalNode::Observable { index, .. }
685 if value.output == ObservableOutput::Corrected =>
686 {
687 index.as_ref()
688 }
689 ClassicalNode::Compute { expr } => return self.condition(level, node, expr),
690 _ => None,
691 };
692 let observable = observable.ok_or(ResolveError::ConditionUnresolved { node })?;
693 match self.assignment {
694 ClassicalAssignment::Uniform(pin) => Ok(pin),
697 ClassicalAssignment::Pinned { forced_observables } => forced_observables
698 .get(observable)
699 .copied()
700 .ok_or(ResolveError::ConditionUnresolved { node }),
701 }
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 fn measurement(instance: u32, measurement: u32) -> InstanceMeasurement {
710 InstanceMeasurement {
711 instance: crate::TemplateInstanceId(instance),
712 measurement,
713 }
714 }
715
716 #[test]
717 fn unsupported_flip_ports_fail_in_value_queries_even_when_inactive() {
718 for classical in [
719 ClassicalNode::Compute {
720 expr: ClassicalExpr::Const(true),
721 },
722 ClassicalNode::fragment(),
723 ] {
724 for active in [None, Some(false), Some(true)] {
725 let mut program = Bloq::new();
726 let mut producer = crate::BloqNode::classical(classical.clone());
727 producer.activation = active.map(|_| 0);
728 let node = program.add_node(producer);
729 if let Some(active) = active {
730 let guard =
731 program.add_node(crate::BloqNode::classical(ClassicalNode::Compute {
732 expr: ClassicalExpr::Const(active),
733 }));
734 program.add_edge(guard, node, crate::BloqEdge::value(0));
735 }
736 let value = ValueRef {
737 node,
738 output: ObservableOutput::Flip,
739 };
740 let assignment = ClassicalAssignment::Uniform(false);
741 let expected = ResolveError::UnsupportedNode { node };
742 assert_eq!(
743 program.classical_ref_value(value, assignment),
744 Err(expected.clone())
745 );
746 assert_eq!(
747 program.resolve_value(value, assignment),
748 Err(expected.clone())
749 );
750 assert_eq!(
751 program.measurement_value_dependencies(value, assignment),
752 Err(expected)
753 );
754 }
755 }
756 }
757
758 fn program() -> Bloq {
761 Bloq::from_text(
762 "\
763BLOQIR 1
764
765template t0 {
766 circuit {
767 MPP X(0,0):m0
768 MPP X(2,0):m1
769 }
770}
771
772graph {
773 n0 quantum {
774 instance i0 t0 @ (0,0)
775 }
776 n1 observable fragment measurements i0:m0 from generator 0
777 n2 observable fragment measurements i0:m1 from generator 1
778 n3 observable 0
779 n4 compute in0
780 n5 compute !in0 from frame x (0,0,1)
781 n6 rus in0 {
782 body {
783 n0 observable fragment measurements i0:m0 from generator 2
784 result n0
785 }
786 }
787 n7 compute in0 ^ in1
788 n1 -> n3 compose 0
789 n2 -> n3 compose 1
790 n3 -> n4 value 0
791 n4 -> n5 value 0
792 n4 -> n6 value 0
793 n5 -> n7 value 0
794 n6 -> n7 value 1
795}
796",
797 )
798 .expect("valid .bloqir text")
799 }
800
801 #[test]
802 fn a_region_result_is_independent_of_internal_consumers_and_unused_values() {
803 let mut program = Bloq::from_text(
804 "BLOQIR 1
805 graph {
806 n0 rus 0 {
807 body {
808 n0 compute 1
809 n1 observable 0
810 n2 compute !in0
811 n0 -> n1 value 0
812 n1 -> n2 value 0
813 result n1
814 }
815 }
816 n1 compute in0
817 n0 -> n1 value 0
818 }
819",
820 )
821 .unwrap();
822 program.validate().unwrap();
823 assert!(
824 program
825 .resolve_classical(BloqNodeId(1), ClassicalAssignment::Uniform(true))
826 .unwrap()
827 .sign
828 );
829 let BloqNodeKind::Region(RegionNode::RepeatUntilSuccess { body, .. }) =
830 &mut program.top_mut().node_mut(BloqNodeId(0)).unwrap().kind
831 else {
832 unreachable!()
833 };
834 body.add_node(crate::BloqNode::classical(ClassicalNode::Compute {
835 expr: ClassicalExpr::Const(false),
836 }));
837 program.validate().unwrap();
838 assert!(
839 program
840 .resolve_classical(BloqNodeId(1), ClassicalAssignment::Uniform(true))
841 .unwrap()
842 .sign
843 );
844 }
845
846 #[test]
847 fn resolution_folds_accumulates_through_a_decoded_frame() {
848 let resolution = program()
849 .resolve_classical(BloqNodeId(5), ClassicalAssignment::Uniform(false))
850 .expect("the frame compute resolves");
851
852 assert_eq!(
853 resolution.measurements,
854 BTreeSet::from([measurement(0, 0), measurement(0, 1)])
855 );
856 assert!(resolution.sign, "the frame compute negates its input");
857 assert_eq!(resolution.decoder_observables, BTreeSet::from([0]));
858 }
859
860 #[test]
861 fn composition_preserves_prior_folds_without_the_childs_decoder_flip() {
862 let mut program = Bloq::new();
863 let earlier = program.add_node(crate::BloqNode::classical(ClassicalNode::Observable {
864 index: Some(0),
865 measurements: vec![measurement(0, 0)],
866 operators: Vec::new(),
867 }));
868 let child = program.add_node(crate::BloqNode::classical(ClassicalNode::Observable {
869 index: Some(1),
870 measurements: vec![measurement(0, 1)],
871 operators: Vec::new(),
872 }));
873 program.add_edge(
874 earlier,
875 child,
876 crate::BloqEdge::Value {
877 slot: 0,
878 role: crate::ValueRole::ReadoutFold,
879 output: ObservableOutput::Corrected,
880 },
881 );
882 let parent = program.add_node(crate::BloqNode::classical(ClassicalNode::observable(2)));
883 program.add_edge(
884 child,
885 parent,
886 crate::BloqEdge::Compose {
887 slot: 0,
888 role: crate::ValueRole::Data,
889 },
890 );
891 let assignment = ClassicalAssignment::Uniform(false);
892 let resolution = program.resolve_classical(parent, assignment).unwrap();
893 assert_eq!(
894 resolution.measurements,
895 BTreeSet::from([measurement(0, 0), measurement(0, 1)])
896 );
897 assert_eq!(resolution.decoder_observables, BTreeSet::from([0, 2]));
898 let flip = ValueRef {
899 node: parent,
900 output: ObservableOutput::Flip,
901 };
902 let resolution = program.resolve_value(flip, assignment).unwrap();
903 assert!(resolution.measurements.is_empty());
904 assert_eq!(resolution.decoder_observables, BTreeSet::from([2]));
905 assert_eq!(
906 program
907 .measurement_value_dependencies(flip, assignment)
908 .unwrap(),
909 BTreeSet::from([measurement(0, 0), measurement(0, 1)])
910 );
911 let forced_observables = BTreeMap::from([(2, true)]);
912 let pinned = ClassicalAssignment::Pinned {
913 forced_observables: &forced_observables,
914 };
915 assert_eq!(program.classical_value(parent, pinned), Ok(true));
916 assert_eq!(
917 program.classical_ref_value(flip, pinned),
918 Err(ResolveError::ConditionUnresolved { node: parent })
919 );
920 }
921
922 #[test]
923 fn composition_preserves_the_regions_selected_result_port() {
924 for output in [ObservableOutput::Corrected, ObservableOutput::Flip] {
925 let mut program = Bloq::new();
926 let mut body = SubGraph::new();
927 let child = body.add_node(crate::BloqNode::classical(ClassicalNode::Observable {
928 index: Some(0),
929 measurements: vec![measurement(0, 0)],
930 operators: Vec::new(),
931 }));
932 body.set_value_output(Some(ValueRef {
933 node: child,
934 output,
935 }));
936 let region =
937 program.add_node(crate::BloqNode::region(RegionNode::RepeatUntilSuccess {
938 restart_source: None,
939 restart_condition: ClassicalExpr::Const(false),
940 body,
941 }));
942 let parent = program.add_node(crate::BloqNode::classical(ClassicalNode::observable(1)));
943 program.add_edge(region, parent, crate::BloqEdge::compose(0));
944 let assignment = ClassicalAssignment::Uniform(false);
945 let value = program.resolve_classical(parent, assignment).unwrap();
946 assert_eq!(
947 value.measurements,
948 if output == ObservableOutput::Flip {
949 BTreeSet::new()
950 } else {
951 BTreeSet::from([measurement(0, 0)])
952 }
953 );
954 assert_eq!(value.decoder_observables, BTreeSet::from([0, 1]));
955 assert_eq!(
956 program
957 .measurement_dependencies(parent, assignment)
958 .unwrap(),
959 BTreeSet::from([measurement(0, 0)])
960 );
961 }
962 }
963
964 #[test]
965 fn inactive_observable_has_no_prediction_or_flip() {
966 let mut program = Bloq::new();
967 let guard = program.add_node(crate::BloqNode::classical(ClassicalNode::Compute {
968 expr: ClassicalExpr::Const(false),
969 }));
970 let mut observable = crate::BloqNode::classical(ClassicalNode::Observable {
971 index: Some(0),
972 measurements: vec![measurement(0, 0)],
973 operators: Vec::new(),
974 });
975 observable.activation = Some(0);
976 let node = program.add_node(observable);
977 program.add_edge(guard, node, crate::BloqEdge::value(0));
978 for output in [ObservableOutput::Corrected, ObservableOutput::Flip] {
979 let value = ValueRef { node, output };
980 let resolution = program
981 .resolve_value(value, ClassicalAssignment::Uniform(true))
982 .unwrap();
983 assert!(resolution.measurements.is_empty());
984 assert!(resolution.decoder_observables.is_empty());
985 assert_eq!(
986 program.classical_ref_value(value, ClassicalAssignment::Uniform(true)),
987 Ok(false)
988 );
989 }
990 }
991
992 #[test]
996 fn shared_producers_cancel_under_xor() {
997 let forced_observables = BTreeMap::from([(0, true)]);
998
999 let resolution = program()
1000 .resolve_classical(
1001 BloqNodeId(7),
1002 ClassicalAssignment::Pinned {
1003 forced_observables: &forced_observables,
1004 },
1005 )
1006 .expect("the xor resolves");
1007
1008 assert_eq!(resolution.measurements, BTreeSet::from([measurement(0, 1)]));
1009 assert!(resolution.sign);
1010 }
1011
1012 #[test]
1014 fn forced_observables_determine_downstream_conditions_and_frames() {
1015 let program = program();
1016
1017 for decoded in [false, true] {
1018 let forced_observables = BTreeMap::from([(0, decoded)]);
1019 let assignment = ClassicalAssignment::Pinned {
1020 forced_observables: &forced_observables,
1021 };
1022
1023 assert_eq!(
1024 program.classical_value(BloqNodeId(4), assignment),
1025 Ok(decoded)
1026 );
1027 assert_eq!(
1028 program.classical_value(BloqNodeId(5), assignment),
1029 Ok(!decoded),
1030 "the frame compute negates what it reads"
1031 );
1032 }
1033 }
1034
1035 #[test]
1036 fn a_value_the_path_does_not_fix_is_an_error_not_a_guess() {
1037 let program = program();
1038 let forced_observables = BTreeMap::new();
1039 let assignment = ClassicalAssignment::Pinned {
1040 forced_observables: &forced_observables,
1041 };
1042
1043 assert_eq!(
1046 program.classical_value(BloqNodeId(4), assignment),
1047 Err(ResolveError::ConditionUnresolved {
1048 node: BloqNodeId(3)
1049 })
1050 );
1051 assert_eq!(
1052 program.classical_value(BloqNodeId(0), assignment),
1053 Err(ResolveError::NotClassical {
1054 node: BloqNodeId(0)
1055 })
1056 );
1057 let forced_observables = BTreeMap::from([(0, true)]);
1060 assert_eq!(
1061 program.classical_value(
1062 BloqNodeId(7),
1063 ClassicalAssignment::Pinned {
1064 forced_observables: &forced_observables,
1065 },
1066 ),
1067 Err(ResolveError::NotClassical {
1068 node: BloqNodeId(6)
1069 })
1070 );
1071 }
1072
1073 #[test]
1074 fn a_uniform_path_fixes_every_predicate_leaf_to_the_same_bit() {
1075 let program = program();
1076
1077 for pin in [false, true] {
1078 assert_eq!(
1079 program.classical_value(BloqNodeId(5), ClassicalAssignment::Uniform(pin)),
1080 Ok(!pin)
1081 );
1082 assert_eq!(
1083 program.classical_value(BloqNodeId(4), ClassicalAssignment::Uniform(pin)),
1084 Ok(pin)
1085 );
1086 }
1087 }
1088
1089 #[test]
1090 fn source_free_retry_region_preserves_its_body_result_without_a_dummy_value() {
1091 let program = Bloq::from_text(
1092 "BLOQIR 1
1093 graph {
1094 n0 rus 0 {
1095 body {
1096 n0 observable 0
1097 result n0
1098 }
1099 }
1100 result n0
1101 }",
1102 )
1103 .unwrap();
1104 program.validate().unwrap();
1105 let pinned = program.pin_membership(&BTreeMap::new()).unwrap();
1106 pinned.validate().unwrap();
1107 let BloqNodeKind::Region(RegionNode::RepeatUntilSuccess {
1108 body,
1109 restart_source,
1110 ..
1111 }) = &pinned[BloqNodeId(0)].kind
1112 else {
1113 panic!("retry region retained")
1114 };
1115 assert!(restart_source.is_none());
1116 assert_eq!(body.nodes().count(), 1);
1117 let assignment = ClassicalAssignment::Uniform(false);
1118 let resolved = pinned.resolve_classical(BloqNodeId(0), assignment).unwrap();
1119 assert!(resolved.measurements.is_empty());
1120 assert_eq!(resolved.decoder_observables, BTreeSet::from([0]));
1121 assert!(
1122 pinned
1123 .measurement_dependencies(BloqNodeId(0), assignment)
1124 .unwrap()
1125 .is_empty()
1126 );
1127 }
1128
1129 #[test]
1131 fn a_restart_predicate_is_not_a_path_constant() {
1132 let program = Bloq::from_text(
1133 "\
1134BLOQIR 1
1135
1136graph {
1137 n0 compute 1
1138 n1 rus in0 source n0 {
1139 body {
1140 n0 compute 0
1141 }
1142 }
1143 n0 -> n1 value 0
1144}
1145",
1146 )
1147 .expect("valid .bloqir text");
1148
1149 assert_eq!(
1150 program.classical_value(BloqNodeId(1), ClassicalAssignment::Uniform(false)),
1151 Err(ResolveError::UnsupportedNode {
1152 node: BloqNodeId(1)
1153 })
1154 );
1155 assert_eq!(
1156 program.classical_value(BloqNodeId(0), ClassicalAssignment::Uniform(false)),
1157 Ok(true),
1158 "a constant compute folds without consulting the path"
1159 );
1160 }
1161
1162 #[test]
1163 fn shared_dags_cache_recipes_and_predicate_values() {
1164 let mut program = program();
1165 let mut recipe = BloqNodeId(1);
1166 let mut predicate = program.add_node(crate::BloqNode::classical(ClassicalNode::Compute {
1167 expr: ClassicalExpr::Const(true),
1168 }));
1169 for _ in 0..30 {
1170 for previous in [&mut recipe, &mut predicate] {
1171 let next = program.add_node(crate::BloqNode::classical(ClassicalNode::Compute {
1172 expr: ClassicalExpr::Xor(Box::new([
1173 ClassicalExpr::In(0),
1174 ClassicalExpr::In(1),
1175 ])),
1176 }));
1177 program.add_edge(*previous, next, crate::BloqEdge::value(0));
1178 program.add_edge(*previous, next, crate::BloqEdge::value(1));
1179 *previous = next;
1180 }
1181 }
1182 let resolution = program
1183 .resolve_classical(recipe, ClassicalAssignment::Uniform(false))
1184 .unwrap();
1185 assert!(resolution.measurements.is_empty());
1186 assert!(!resolution.sign);
1187 assert_eq!(
1188 program.classical_value(
1189 predicate,
1190 ClassicalAssignment::Pinned {
1191 forced_observables: &BTreeMap::new(),
1192 }
1193 ),
1194 Ok(false)
1195 );
1196 }
1197
1198 #[test]
1199 fn predicate_cycles_return_the_same_typed_error_as_record_cycles() {
1200 let program = Bloq::from_text(
1201 "BLOQIR 1
1202 graph {
1203 n0 compute in0
1204 n1 compute in0
1205 n2 rus 0 when v0 {
1206 body {
1207 }
1208 }
1209 n0 -> n1 value 0
1210 n1 -> n0 value 0
1211 n0 -> n2 value 0
1212 }
1213",
1214 )
1215 .unwrap();
1216 let forced_observables = BTreeMap::new();
1217 let assignment = ClassicalAssignment::Pinned {
1218 forced_observables: &forced_observables,
1219 };
1220 let expected = Err(ResolveError::CyclicValueGraph {
1221 node: BloqNodeId(0),
1222 });
1223 assert_eq!(program.classical_value(BloqNodeId(0), assignment), expected);
1224 for node in [BloqNodeId(0), BloqNodeId(2)] {
1225 assert_eq!(
1226 program.resolve_classical(node, assignment).map(|_| false),
1227 expected
1228 );
1229 }
1230 }
1231
1232 #[test]
1233 fn dependency_folds_never_cancel_shared_measurements() {
1234 let mut program = Bloq::from_text(
1235 "BLOQIR 1
1236 template t0 {
1237 circuit {
1238 M (0,0):m0
1239 M (1,0):m1
1240 }
1241 }
1242 graph {
1243 n0 quantum {
1244 instance i0 t0 @ (0,0)
1245 }
1246 n1 observable fragment measurements i0:m0
1247 n2 observable fragment measurements i0:m1
1248 n3 compute (in0 & in1) ^ in0
1249 n4 observable 0
1250 n0 -> n1 order
1251 n0 -> n2 order
1252 n1 -> n3 value 0
1253 n2 -> n3 value 1
1254 n3 -> n4 value 0
1255 n2 -> n4 compose 1
1256 }
1257",
1258 )
1259 .unwrap();
1260 program.validate().unwrap();
1261 for expr in [
1262 program[BloqNodeId(3)].try_classical().unwrap().clone(),
1263 ClassicalNode::Compute {
1264 expr: ClassicalExpr::select(
1265 ClassicalExpr::In(0),
1266 ClassicalExpr::In(1),
1267 ClassicalExpr::In(0),
1268 ),
1269 },
1270 ClassicalNode::Compute {
1271 expr: ClassicalExpr::And(Box::new([
1272 ClassicalExpr::In(0),
1273 ClassicalExpr::In(1),
1274 ClassicalExpr::In(0),
1275 ])),
1276 },
1277 ] {
1278 program.node_mut(BloqNodeId(3)).unwrap().kind = BloqNodeKind::Classical(expr.into());
1279 program.validate().unwrap();
1280 for node in [BloqNodeId(3), BloqNodeId(4)] {
1281 let expected = BTreeSet::from([measurement(0, 0), measurement(0, 1)]);
1282 assert_eq!(
1283 program
1284 .measurement_dependencies(node, ClassicalAssignment::Uniform(false))
1285 .unwrap(),
1286 expected
1287 );
1288 }
1289 }
1290 }
1291
1292 #[test]
1293 fn fixed_path_cofactors_select_and_fully_fixed_boolean_ops() {
1294 let program = Bloq::from_text(
1295 "\
1296BLOQIR 1
1297
1298template t0 {
1299 circuit {
1300 M (0,0):m0
1301 M (1,0):m1
1302 M (2,0):m2
1303 }
1304}
1305
1306graph {
1307 n0 quantum {
1308 instance i0 t0 @ (0,0)
1309 }
1310 n1 observable fragment measurements i0:m0
1311 n2 observable 0
1312 n3 compute in0
1313 n4 observable fragment measurements i0:m1
1314 n5 observable 1
1315 n6 compute in0
1316 n7 observable fragment measurements i0:m2
1317 n8 compute select(in0, in1, in2)
1318 n9 compute in0 & in1
1319 n10 compute in0 | in1
1320 n11 compute in0 ^ in1
1321 n12 compute in0 ^ in1
1322 n1 -> n2 compose 0
1323 n2 -> n3 value 0
1324 n4 -> n5 compose 0
1325 n5 -> n6 value 0
1326 n3 -> n8 value 0
1327 n7 -> n8 value 1
1328 n6 -> n8 value 2
1329 n3 -> n9 value 0
1330 n6 -> n9 value 1
1331 n3 -> n10 value 0
1332 n6 -> n10 value 1
1333 n3 -> n11 value 0
1334 n3 -> n11 value 1
1335 n3 -> n12 value 0
1336 n6 -> n12 value 1
1337}
1338",
1339 )
1340 .expect("valid .bloqir text");
1341 let forced_observables = BTreeMap::from([(0, true), (1, false)]);
1342 let assignment = ClassicalAssignment::Pinned {
1343 forced_observables: &forced_observables,
1344 };
1345
1346 let selected = program
1347 .resolve_classical(BloqNodeId(8), assignment)
1348 .unwrap();
1349 assert_eq!(selected.measurements, BTreeSet::from([measurement(0, 1)]));
1350 assert!(!selected.sign);
1351 assert_eq!(selected.decoder_observables, BTreeSet::from([1]));
1352 for (node, value) in [(BloqNodeId(9), false), (BloqNodeId(10), true)] {
1353 let resolution = program.resolve_classical(node, assignment).unwrap();
1354 assert!(resolution.measurements.is_empty());
1355 assert_eq!(resolution.sign, value);
1356 assert!(resolution.decoder_observables.is_empty());
1357 assert_eq!(program.classical_value(node, assignment), Ok(value));
1358 }
1359 let cancelled = program
1360 .resolve_classical(BloqNodeId(11), assignment)
1361 .unwrap();
1362 assert!(cancelled.measurements.is_empty());
1363 assert!(cancelled.decoder_observables.is_empty());
1364 let multiple = program
1365 .resolve_classical(BloqNodeId(12), assignment)
1366 .unwrap();
1367 assert_eq!(
1368 multiple.measurements,
1369 BTreeSet::from([measurement(0, 0), measurement(0, 1)])
1370 );
1371 assert_eq!(multiple.decoder_observables, BTreeSet::from([0, 1]));
1372
1373 assert_eq!(
1377 program.classical_value(BloqNodeId(8), assignment),
1378 Err(ResolveError::ConditionUnresolved {
1379 node: BloqNodeId(7)
1380 })
1381 );
1382 assert_eq!(
1383 program
1384 .measurement_dependencies(BloqNodeId(8), assignment)
1385 .unwrap(),
1386 BTreeSet::from([measurement(0, 0), measurement(0, 1), measurement(0, 2),])
1387 );
1388 for node in [BloqNodeId(9), BloqNodeId(10)] {
1389 assert_eq!(
1390 program.measurement_dependencies(node, assignment).unwrap(),
1391 BTreeSet::from([measurement(0, 0), measurement(0, 1)])
1392 );
1393 }
1394
1395 for (node, fixed) in [(BloqNodeId(9), false), (BloqNodeId(10), true)] {
1398 let forced_observables = BTreeMap::from([(0, fixed)]);
1399 let partial = ClassicalAssignment::Pinned {
1400 forced_observables: &forced_observables,
1401 };
1402 assert_eq!(
1403 program.resolve_classical(node, partial),
1404 Err(ResolveError::NonLinearExpr { node })
1405 );
1406 assert_eq!(
1407 program.classical_value(node, partial),
1408 Err(ResolveError::ConditionUnresolved {
1409 node: BloqNodeId(5)
1410 })
1411 );
1412 }
1413
1414 let forced_observables = BTreeMap::new();
1415 let unresolved = ClassicalAssignment::Pinned {
1416 forced_observables: &forced_observables,
1417 };
1418 for node in [BloqNodeId(8), BloqNodeId(9), BloqNodeId(10)] {
1419 assert_eq!(
1420 program.resolve_classical(node, unresolved),
1421 Err(ResolveError::NonLinearExpr { node })
1422 );
1423 }
1424 }
1425
1426 #[test]
1427 fn an_unfixed_non_linear_expression_has_no_measurement_recipe() {
1428 let program = Bloq::from_text(
1429 "\
1430BLOQIR 1
1431
1432template t0 {
1433 circuit {
1434 MPP X(0,0):m0
1435 }
1436}
1437
1438graph {
1439 n0 quantum {
1440 instance i0 t0 @ (0,0)
1441 }
1442 n1 observable fragment measurements i0:m0
1443 n2 compute 1
1444 n3 compute in0 & in1
1445 n1 -> n3 value 0
1446 n2 -> n3 value 1
1447}
1448",
1449 )
1450 .expect("valid .bloqir text");
1451
1452 let forced_observables = BTreeMap::new();
1453 let assignment = ClassicalAssignment::Pinned {
1454 forced_observables: &forced_observables,
1455 };
1456 assert_eq!(
1457 program.resolve_classical(BloqNodeId(3), assignment),
1458 Err(ResolveError::NonLinearExpr {
1459 node: BloqNodeId(3)
1460 })
1461 );
1462 assert_eq!(
1463 program.classical_value(BloqNodeId(3), assignment),
1464 Err(ResolveError::ConditionUnresolved {
1465 node: BloqNodeId(1)
1466 })
1467 );
1468 assert_eq!(
1469 program
1470 .measurement_dependencies(BloqNodeId(3), assignment)
1471 .unwrap(),
1472 BTreeSet::from([measurement(0, 0)])
1473 );
1474 }
1475}