Skip to main content

bloq_ir/
pin.rs

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