Skip to main content

bloq_ir/
resolve.rs

1//! Static evaluation of the classical dataflow graph under a fixed path.
2//!
3//! A compiled program's classical nodes describe *how* an observable's parity
4//! is assembled from measurement records, not what it evaluates to — the bits
5//! only exist per shot. What an offline consumer needs is the recipe: which
6//! measurement sites XOR into a given node's value, and with what sign. That
7//! is a fold over [`ClassicalNode`]/[`ClassicalExpr`], and every consumer that
8//! needed it reimplemented the same recursion.
9//!
10//! Folding requires a *fixed path* for activation and nonlinear expressions.
11//! [`ClassicalAssignment`] supplies their predicate values;
12//! resolution fails rather than guessing when the path does not determine it.
13//!
14//! The XOR-expressible fragment resolves directly (SEM-FOLD). On a fixed path,
15//! `Select` resolves through its selected arm and a fully fixed `And`/`Or`
16//! resolves to a constant; unresolved nonlinear values are rejected instead of
17//! approximated.
18//!
19//! [`Bloq::measurement_dependencies`] asks the same fold a weaker question —
20//! *which measurements does this value wait on* rather than *what parity is it*
21//! — so it traces Flip queries and unions nonlinear operands.
22
23use 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/// The fixed path a [`Bloq::resolve_classical`] fold evaluates under.
33///
34/// Predicate inputs are fixed uniformly or through known observable values.
35#[derive(Debug, Clone, Copy)]
36pub enum ClassicalAssignment<'a> {
37    /// Every predicate input and observable leaf folds to the same bit.
38    Uniform(bool),
39    /// Observable values fixing activation and expressions through their inputs.
40    /// A condition that reaches an unknown value is an error, not a default.
41    Pinned {
42        /// Values for [`ClassicalNode::Observable`] indices this path fixes,
43        /// keyed by observable index. These are corrected values; they do not
44        /// fix a decoder flip.
45        forced_observables: &'a BTreeMap<u32, bool>,
46    },
47}
48
49/// Whether a fold derives an XOR recipe or only measurement dependencies.
50#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
51enum FoldMode {
52    #[default]
53    Parity,
54    MeasurementDependencies,
55}
56
57/// What a classical node's value is made of, under one fixed path.
58///
59/// `measurements` and the named decoder flips XORed together, then negated when
60/// `sign` is set, reproduce the node's per-shot bit.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct ClassicalResolution {
63    /// The measurement sites whose parity the node's value is.
64    pub measurements: BTreeSet<InstanceMeasurement>,
65    /// The constant the parity is XORed with: `true` inverts.
66    pub sign: bool,
67    /// Decoder-flip observables XORed into this value, in ascending order.
68    /// Repeated occurrences cancel like repeated measurements.
69    pub decoder_observables: BTreeSet<u32>,
70}
71
72/// Why a classical fold could not be completed.
73#[derive(Debug, Clone, PartialEq, Eq, Error)]
74#[non_exhaustive]
75pub enum ResolveError {
76    /// The requested node is missing.
77    #[error("node {node:?} does not exist at the level being resolved")]
78    MissingNode {
79        /// Missing node.
80        node: BloqNodeId,
81    },
82    /// A fold reached a quantum node.
83    #[error("classical fold reached quantum node {node:?}")]
84    NotClassical {
85        /// Quantum node.
86        node: BloqNodeId,
87    },
88    /// A classical node has no statically foldable value.
89    #[error("classical node {node:?} has no value a static fold can produce")]
90    UnsupportedNode {
91        /// Unsupported node.
92        node: BloqNodeId,
93    },
94    /// An expression input slot is unwired.
95    #[error("node {node:?} reads slot {slot}, which has no producer")]
96    UnwiredSlot {
97        /// Consumer node.
98        node: BloqNodeId,
99        /// Missing slot.
100        slot: u32,
101    },
102    /// An expression has no affine measurement recipe.
103    #[error("node {node:?} folds a non-linear expression, which has no measurement recipe")]
104    NonLinearExpr {
105        /// Nonlinear node.
106        node: BloqNodeId,
107    },
108    /// The selected path does not fix a condition.
109    #[error("condition at node {node:?} is not fixed by the resolved path")]
110    ConditionUnresolved {
111        /// Unresolved node.
112        node: BloqNodeId,
113    },
114    /// Classical value dependencies are cyclic.
115    #[error("value graph cycles through node {node:?}")]
116    CyclicValueGraph {
117        /// Node closing the cycle.
118        node: BloqNodeId,
119    },
120}
121
122/// A node's affine value: physical measurement parity plus decoder-flip parity.
123#[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
167/// The producers wired into `node`'s value slots, keyed by slot.
168///
169/// WF-2 makes a slot's producer unique, so the map is total over the fed
170/// slots.
171fn 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    /// Resolve a level-local node's affine physical/decoder recipe under a fixed path.
200    ///
201    /// Uses the same semantics and errors as [`Bloq::resolve_classical`].
202    ///
203    /// # Errors
204    ///
205    /// Returns [`ResolveError`] when the path cannot resolve the node to an
206    /// affine physical/decoder parity.
207    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    /// Resolve a selected runtime output under a fixed path.
216    ///
217    /// # Errors
218    /// Returns the same errors as [`Self::resolve_classical`].
219    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    /// The physical-measurement and decoder-flip recipe behind a top-level
236    /// classical node's value, folded under a fixed path.
237    ///
238    /// Follows the node's `Value` inputs recursively, descending into a region
239    /// under `assignment` and out through that body's declared
240    /// bit result (SEM-RVAL). `Observable` fragments and complete observables
241    /// contribute measurements; `Const` leaves contribute sign. An observable
242    /// also contributes its named decoder flip on its Corrected output;
243    /// composition includes only its recipe.
244    ///
245    /// # Errors
246    ///
247    /// Returns [`ResolveError`] when the path does not determine a region or
248    /// nonlinear cofactor, or when the fold reaches a node with no static value
249    /// (a quantum node or a `Discard`).
250    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    /// Resolve a selected runtime output under a fixed path.
259    ///
260    /// # Errors
261    /// Returns the same errors as [`Self::resolve_classical`].
262    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    /// Which measurement records `node`'s value waits on, rather than what
271    /// parity it is.
272    ///
273    /// Unlike the parity fold, this conservatively unions all record dependencies,
274    /// including records needed for Flip outputs and nonlinear expressions.
275    ///
276    /// # Errors
277    /// Returns the same errors as [`Self::resolve_classical`].
278    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    /// Measurement dependencies of a selected runtime output, including Flip.
287    ///
288    /// # Errors
289    /// Returns the same errors as [`Self::measurement_dependencies`].
290    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    /// The bit a top-level node already takes on a fixed path.
303    ///
304    /// Where [`Self::resolve_classical`] answers *which measurements* a value
305    /// is assembled from, this answers *what that value is* — for the nodes a
306    /// path determines offline, with no per-shot record involved. A
307    /// `Observable` reads its corrected bit from the path's
308    /// `forced_observables`, and a `Compute` folds its own expression over
309    /// those leaves.
310    ///
311    /// Under [`ClassicalAssignment::Uniform`] every predicate leaf folds to
312    /// the uniform bit, matching how a uniform path fixes predicate inputs.
313    ///
314    /// # Errors
315    ///
316    /// Returns [`ResolveError`] when the node is quantum, when it is a
317    /// `RepeatUntilSuccess` region (whose restart predicate is per-attempt
318    /// decoder state, not a path constant), or when the fold reaches a node
319    /// the path does not fix — a fragment's per-shot record, or an
320    /// observable with no forced value.
321    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    /// A selected output's bit when fixed by the supplied path.
330    ///
331    /// # Errors
332    /// Returns the same errors as [`Self::classical_value`].
333    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    /// The nodes currently being folded, as `(level, node, output)`. Levels are
357    /// compared by address because a [`BloqNodeId`] only identifies a node
358    /// within its own level. Guards against a cycle in a hand-built or
359    /// text-loaded program, which validation would reject (WF-1) but this fold
360    /// may run before.
361    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    /// A retry region's exported value: its accepted body's declared bit producer
498    /// (SEM-RVAL), or the XOR identity when the body exports none.
499    fn region_value(&mut self, region: &RegionNode) -> Result<Value, ResolveError> {
500        // Only the accepted attempt is observable, so the body folds once.
501        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    /// Fold a region predicate to the bit it takes on the resolved path.
571    ///
572    /// Unlike [`Self::expression`] this yields a plain bit, so an unresolved
573    /// condition is an error.
574    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                // A uniform path fixes every condition directly; there is no
589                // observable table to resolve the producer against.
590                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    /// A Boolean cofactor needed to turn a nonlinear expression into an exact
621    /// fixed-path parity. An unavailable predicate means the nonlinear value
622    /// still has no affine measurement recipe.
623    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    /// Fold the value a predicate reads out of one classical producer.
637    ///
638    /// The pinned path reaches this through a condition's `In` slot; a caller
639    /// asking [`Bloq::classical_value`] for a node's own bit enters here
640    /// directly, which is why the uniform path is handled rather than
641    /// unreachable.
642    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            // A uniform path fixes every predicate leaf to the same bit, and
695            // carries no observable table to look one up in.
696            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    /// Two fragments feed an observable whose corrected parity is inverted by
759    /// a frame Compute. A retry region exports a third fragment from its accepted attempt.
760    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    /// The XOR fold cancels: the frame and the retry body share `i0:m0`, so
993    /// resolving their XOR sees neither. A resolver that deduplicated visited
994    /// nodes instead of re-folding them would keep the record and be wrong.
995    #[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    /// Forced observables also determine downstream conditions and frames.
1013    #[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        // No value for observable 0, so nothing downstream of the observable is
1044        // fixed.
1045        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        // n7 reads the retry region's exported value, which is a measurement
1058        // recipe rather than a bit: that is `resolve_classical`'s question.
1059        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    /// A retry region's predicate is independent of its exported value.
1130    #[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        // Value evaluation remains strict: even the unselected raw-record arm
1374        // must be available, while a fixed-path parity reads only its selected
1375        // arm above.
1376        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        // Boolean evaluation stays strict instead of short-circuiting around
1396        // an unavailable operand.
1397        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}