Skip to main content

bloq_graph/
action_dag.rs

1//! Validated classical-action dependencies and reachable value domains.
2
3pub(crate) mod hierarchy;
4mod svg;
5
6use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
7
8use bloq_utils::PauliBasis;
9use glam::IVec3;
10use petgraph::algo::toposort;
11use petgraph::stable_graph::{NodeIndex, StableDiGraph};
12use petgraph::visit::{EdgeRef, IntoEdgeReferences};
13use rand::{Rng, RngExt};
14
15use crate::validate::InvalidActionError;
16use crate::{
17    Action, BlockGraph, BlockGraphError, BlockKind, BranchRegion, Expr, FeedbackTarget,
18    MeasureTarget, SelectiveKind, Stabilizer, StabilizerGenerator, selective_fixings,
19};
20
21/// Distinct jointly reachable values of an ordered set of Boolean action
22/// conditions.
23///
24/// This is the canonical branch domain shared by stabilizer construction,
25/// lowering, editor pinning, and public reachability queries. Values use the
26/// same target order supplied at construction.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ResolveValueDomain {
29    values: Vec<Vec<bool>>,
30}
31
32/// A bounded value query exhausted Boolean work or found too many values.
33#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
34#[non_exhaustive]
35pub enum ResolveDomainError {
36    /// Boolean resource limits were exceeded while enumerating values.
37    #[error("{0}")]
38    Resource(#[from] BooleanResourceError),
39    /// The domain exceeded the caller's value-count limit.
40    #[error("resolve domain contains at least {observed} values")]
41    TooManyValues {
42        /// Lower bound on the number of reachable values observed.
43        observed: usize,
44    },
45}
46
47impl ResolveDomainError {
48    /// Retain a caller's domain label without disguising Boolean exhaustion.
49    pub(crate) fn into_stabilizer(
50        self,
51        phase: &'static str,
52        limit: usize,
53    ) -> crate::StabilizerError {
54        match self {
55            Self::Resource(error) => error.into(),
56            Self::TooManyValues { observed } => crate::StabilizerError::ResourceLimited {
57                phase,
58                observed,
59                limit,
60            },
61        }
62    }
63}
64
65impl ResolveValueDomain {
66    /// Distinct reachable value tuples.
67    pub fn values(&self) -> &[Vec<bool>] {
68        &self.values
69    }
70
71    /// Whether `values` is one reachable tuple in this domain.
72    pub fn contains(&self, values: &[bool]) -> bool {
73        self.values
74            .binary_search_by(|candidate| candidate.as_slice().cmp(values))
75            .is_ok()
76    }
77}
78
79use bloq_utils::boolean::{BooleanDecisionDiagram, BooleanLimits, BooleanOp, BooleanResourceError};
80pub(crate) use bloq_utils::boolean::{DECISION_FALSE, DECISION_TRUE, DecisionId};
81
82#[derive(Debug, Default)]
83pub(crate) struct ResolveDecisionDiagram {
84    pub(crate) diagram: BooleanDecisionDiagram,
85    pub(crate) variable_indices: HashMap<String, usize>,
86    alias_cache: HashMap<String, DecisionId>,
87    pub(crate) roots: Vec<DecisionId>,
88}
89
90impl ResolveDecisionDiagram {
91    pub(crate) fn build(
92        exprs: &[&Expr],
93        aliases: &HashMap<&str, &Expr>,
94        limits: BooleanLimits,
95    ) -> Result<Option<Self>, BooleanResourceError> {
96        let mut diagram = Self {
97            diagram: BooleanDecisionDiagram::with_limits(limits),
98            ..Self::default()
99        };
100        for expr in exprs {
101            let Some(root) = diagram.expression(expr, aliases)? else {
102                return Ok(None);
103            };
104            diagram.roots.push(root);
105        }
106        Ok(Some(diagram))
107    }
108
109    fn expression(
110        &mut self,
111        expr: &Expr,
112        aliases: &HashMap<&str, &Expr>,
113    ) -> Result<Option<DecisionId>, BooleanResourceError> {
114        enum Step<'a> {
115            Expr(&'a Expr),
116            Alias(&'a str),
117            Not,
118            Binary(crate::BinaryOp),
119        }
120        let mut pending = vec![Step::Expr(expr)];
121        let mut values = Vec::new();
122        let mut visiting = HashSet::new();
123        while let Some(step) = pending.pop() {
124            self.diagram.charge(1)?;
125            match step {
126                Step::Expr(Expr::Var(name)) => {
127                    if let Some(&root) = self.alias_cache.get(name) {
128                        values.push(root);
129                    } else if let Some(&resolved) = aliases.get(name.as_str()) {
130                        if !visiting.insert(name.as_str()) {
131                            return Ok(None);
132                        }
133                        pending.extend([Step::Alias(name), Step::Expr(resolved)]);
134                    } else {
135                        // First-use order keeps neighboring source controls together;
136                        // lexical names can interleave unrelated sites (bit10 before bit2).
137                        let next = self.variable_indices.len();
138                        let variable = *self.variable_indices.entry(name.clone()).or_insert(next);
139                        values.push(self.make_node(variable, DECISION_FALSE, DECISION_TRUE)?);
140                    }
141                }
142                Step::Expr(Expr::Not(inner)) => {
143                    pending.extend([Step::Not, Step::Expr(inner)]);
144                }
145                Step::Expr(Expr::Binary(operator, left, right)) => {
146                    pending.extend([Step::Binary(*operator), Step::Expr(right), Step::Expr(left)]);
147                }
148                Step::Alias(name) => {
149                    visiting.remove(name);
150                    self.alias_cache.insert(
151                        name.to_owned(),
152                        *values
153                            .last()
154                            .expect("alias expression is evaluated before caching"),
155                    );
156                }
157                Step::Not => {
158                    let value = values.pop().expect("operand is evaluated before negation");
159                    values.push(self.negate(value)?);
160                }
161                Step::Binary(operator) => {
162                    let right = values
163                        .pop()
164                        .expect("right operand is evaluated before its operator");
165                    let left = values
166                        .pop()
167                        .expect("left operand is evaluated before its operator");
168                    values.push(self.apply(operator, left, right)?);
169                }
170            }
171        }
172        Ok(values.pop())
173    }
174
175    pub(crate) fn make_node(
176        &mut self,
177        variable: usize,
178        low: DecisionId,
179        high: DecisionId,
180    ) -> Result<DecisionId, BooleanResourceError> {
181        self.diagram.make_node(variable, low, high)
182    }
183    pub(crate) fn negate(&mut self, id: DecisionId) -> Result<DecisionId, BooleanResourceError> {
184        self.diagram.negate(id)
185    }
186    pub(crate) fn apply(
187        &mut self,
188        op: crate::BinaryOp,
189        left: DecisionId,
190        right: DecisionId,
191    ) -> Result<DecisionId, BooleanResourceError> {
192        let op = match op {
193            crate::BinaryOp::Xor => BooleanOp::Xor,
194            crate::BinaryOp::And => BooleanOp::And,
195            crate::BinaryOp::Or => BooleanOp::Or,
196        };
197        self.diagram.apply(op, left, right)
198    }
199    fn values(&mut self) -> Result<Vec<Vec<bool>>, BooleanResourceError> {
200        self.values_up_to(usize::MAX)
201    }
202    pub(crate) fn values_up_to(
203        &mut self,
204        limit: usize,
205    ) -> Result<Vec<Vec<bool>>, BooleanResourceError> {
206        self.diagram.values_up_to(&self.roots, limit)
207    }
208    fn contains(&mut self, values: &[bool]) -> Result<bool, BooleanResourceError> {
209        if values.len() != self.roots.len() {
210            return Ok(false);
211        }
212        let mut conjunction = DECISION_TRUE;
213        for (index, &value) in values.iter().enumerate() {
214            let root = self.roots[index];
215            let required = if value { root } else { self.negate(root)? };
216            conjunction = self.apply(crate::BinaryOp::And, conjunction, required)?;
217            if conjunction == DECISION_FALSE {
218                return Ok(false);
219            }
220        }
221        Ok(true)
222    }
223}
224/// The Pauli basis a measurement action observes.
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub enum MeasurementObservable {
227    /// A fixed Pauli basis.
228    Concrete(PauliBasis),
229    /// A basis chosen at runtime by resolving a selective block.
230    Selective(SelectiveKind),
231}
232
233/// Why one action must complete before another.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
235pub enum ActionDependency {
236    /// A condition or binding reads a variable produced by the predecessor.
237    Classical,
238    /// A measurement surface crosses a selective site and waits for its resolve.
239    SelectiveSupport,
240    /// A measurement surface crosses a terminal conditional region and waits
241    /// for its structural branch decision.
242    BranchSupport,
243    /// Feedback anticommutes with a measurement surface and changes its value.
244    FeedbackAnticommutation,
245    /// A composed parity reuses an already decoded named readout.
246    ReadoutParity,
247}
248
249impl ActionDependency {
250    /// Whether this dependency was derived from stabilizer support.
251    pub const fn is_implicit(self) -> bool {
252        !matches!(self, Self::Classical)
253    }
254}
255
256/// Error from [`ActionDag::set_measurement_observable`].
257#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
258#[non_exhaustive]
259pub(crate) enum SetMeasurementObservableError {
260    /// The requested action ordinal is outside the action list.
261    #[error("action ordinal {ordinal} is out of range for {len} actions")]
262    OrdinalOutOfRange {
263        /// Requested source action ordinal.
264        ordinal: usize,
265        /// Number of actions in the DAG.
266        len: usize,
267    },
268    /// The requested action is not a measurement.
269    #[error("action at ordinal {ordinal} is not a measurement node")]
270    NotMeasurementNode {
271        /// Source action ordinal.
272        ordinal: usize,
273    },
274}
275
276/// Authored module ownership of an action in a composed dependency graph.
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct ActionOwner {
279    /// Name of the reusable module definition.
280    pub definition: String,
281    /// Qualified instance path, separated by `__`. Empty for the root.
282    pub instance_path: String,
283}
284
285/// A source action and its cached measurement metadata.
286#[derive(Debug, Clone, PartialEq)]
287pub struct ActionNode {
288    /// Position in the standalone source order or composed action list.
289    pub ordinal: usize,
290    /// The action itself.
291    pub action: Action,
292    /// Module definition and instance that own this composed action.
293    /// Absent on a DAG built from a standalone action list or flat graph.
294    /// Interface bindings belong to the receiving instance.
295    pub owner: Option<ActionOwner>,
296    /// Resolved observable, set for measurement nodes during validation.
297    /// Edge records use the stored pipe source in a `BlockGraph` and the smaller
298    /// node ID endpoint in a `ZXGraph`; conversion conjugates across H as needed.
299    pub measurement: Option<MeasurementObservable>,
300    /// Derived stabilizer surface for a measurement node after physical analysis.
301    pub measurement_stabilizer: Option<Stabilizer>,
302}
303
304/// An ordered dependency graph over a program's [`Action`]s.
305///
306/// Nodes preserve source order via [`ActionNode::ordinal`]; edges point from a
307/// definition (a `let` or `measure` binding, or a resolve of a selective block)
308/// to each consumer. Timing is not stored; compiler scheduling also uses graph
309/// topology.
310#[derive(Debug, Clone, Default)]
311pub struct ActionDag {
312    graph: StableDiGraph<ActionNode, ActionDependency>,
313    ordered: Vec<NodeIndex<u32>>,
314    inputs: BTreeSet<String>,
315    branch_regions: Vec<BranchRegion>,
316    analyzed: bool,
317}
318
319impl ActionDag {
320    /// Selective fills needed by this named readout, in coordinate order.
321    /// Returns `None` when the measurement is not in this action graph.
322    pub fn resolve_sites_for_measurement(&self, name: &str) -> Option<Vec<IVec3>> {
323        let consumer = self
324            .ordered_nodes()
325            .find(|node| {
326                matches!(&node.action, Action::Measure { name: measured, .. } if measured == name)
327            })?
328            .ordinal;
329        let mut sites = self
330            .dependencies()
331            .filter_map(|(predecessor, target, dependency)| {
332                if target != consumer || dependency != ActionDependency::SelectiveSupport {
333                    return None;
334                }
335                match self.node_by_ordinal(predecessor)?.action {
336                    Action::Resolve { target, .. } => Some(target),
337                    _ => None,
338                }
339            })
340            .collect::<Vec<_>>();
341        sites.sort_unstable_by_key(IVec3::to_array);
342        sites.dedup();
343        Some(sites)
344    }
345
346    /// Builds the DAG from actions in source order, wiring dependency edges from
347    /// each variable, measurement, or resolve definition to its uses.
348    pub fn from_actions(actions: &[Action]) -> Self {
349        Self::from_actions_with_inputs(actions, std::iter::empty())
350    }
351
352    pub(crate) fn from_actions_with_inputs(
353        actions: &[Action],
354        inputs: impl IntoIterator<Item = String>,
355    ) -> Self {
356        let mut dag = ActionDag::default();
357        dag.inputs.extend(inputs);
358        let mut defs = HashMap::<&str, NodeIndex<u32>>::new();
359        let mut resolve_targets = HashMap::<IVec3, NodeIndex<u32>>::new();
360
361        for (ordinal, action) in actions.iter().enumerate() {
362            let node = dag.graph.add_node(ActionNode {
363                ordinal,
364                action: action.clone(),
365                owner: None,
366                measurement: None,
367                measurement_stabilizer: None,
368            });
369            dag.ordered.push(node);
370
371            if let Some(name) = defines_name(action) {
372                defs.entry(name).or_insert(node);
373            }
374
375            if let Action::Resolve { target, .. } = action {
376                resolve_targets.entry(*target).or_insert(node);
377            }
378        }
379
380        for (ordinal, action) in actions.iter().enumerate() {
381            let node = dag.ordered[ordinal];
382            for used in action.referenced_names() {
383                let Some(&pred) = defs.get(used) else {
384                    continue;
385                };
386                dag.add_dependency(pred, node, ActionDependency::Classical);
387            }
388
389            if let Action::Measure {
390                target: MeasureTarget::Node(pos),
391                ..
392            } = action
393                && let Some(&pred) = resolve_targets.get(pos)
394            {
395                dag.add_dependency(pred, node, ActionDependency::SelectiveSupport);
396            }
397        }
398
399        dag
400    }
401
402    /// External Boolean names accepted by this action program.
403    pub fn inputs(&self) -> impl Iterator<Item = &str> {
404        self.inputs.iter().map(String::as_str)
405    }
406
407    /// Iterates over nodes in source order.
408    pub fn ordered_nodes(&self) -> impl Iterator<Item = &ActionNode> {
409        self.ordered.iter().map(|&idx| &self.graph[idx])
410    }
411
412    /// Returns the validated terminal region owned by `target`, if this DAG
413    /// was built against a source graph containing that branch action.
414    pub(crate) fn branch_region(&self, target: IVec3) -> Option<&BranchRegion> {
415        self.branch_regions
416            .iter()
417            .find(|region| region.target == target)
418    }
419
420    /// Iterates over dependency edges as `(predecessor ordinal, consumer ordinal, reason)`.
421    pub fn dependencies(&self) -> impl Iterator<Item = (usize, usize, ActionDependency)> + '_ {
422        self.graph.edge_references().map(|edge| {
423            (
424                self.graph[edge.source()].ordinal,
425                self.graph[edge.target()].ordinal,
426                *edge.weight(),
427            )
428        })
429    }
430
431    /// Whether correlation-derived dependencies are current.
432    /// An empty program has nothing to derive.
433    pub const fn is_analyzed(&self) -> bool {
434        self.analyzed || self.ordered.is_empty()
435    }
436
437    pub(crate) fn attach_guarded_dependencies(
438        &mut self,
439        edges: impl IntoIterator<Item = (usize, usize, ActionDependency)>,
440    ) -> Result<(), BlockGraphError> {
441        for (from, to, reason) in edges {
442            self.add_dependency(self.ordered[from], self.ordered[to], reason);
443        }
444        self.validate_semantics()?;
445        self.analyzed = true;
446        Ok(())
447    }
448
449    /// Returns the node at the given source ordinal, if any.
450    pub fn node_by_ordinal(&self, ordinal: usize) -> Option<&ActionNode> {
451        self.ordered.get(ordinal).map(|&idx| &self.graph[idx])
452    }
453
454    /// Records the resolved observable for the measurement node at `ordinal`.
455    ///
456    /// # Errors
457    ///
458    /// Returns an error if `ordinal` is out of range or does not name a
459    /// measurement node.
460    pub(crate) fn set_measurement_observable(
461        &mut self,
462        ordinal: usize,
463        measurement: MeasurementObservable,
464    ) -> Result<(), SetMeasurementObservableError> {
465        let Some(&idx) = self.ordered.get(ordinal) else {
466            return Err(SetMeasurementObservableError::OrdinalOutOfRange {
467                ordinal,
468                len: self.ordered.len(),
469            });
470        };
471        if !matches!(self.graph[idx].action, Action::Measure { .. }) {
472            return Err(SetMeasurementObservableError::NotMeasurementNode { ordinal });
473        }
474        self.graph[idx].measurement = Some(measurement);
475        Ok(())
476    }
477
478    /// Returns whether `node` has a direct dependency edge from the action at
479    /// `pred_ordinal`.
480    pub fn depends_on(&self, node: &ActionNode, pred_ordinal: usize) -> bool {
481        let Some(&node_idx) = self.ordered.get(node.ordinal) else {
482            return false;
483        };
484        let Some(&pred_idx) = self.ordered.get(pred_ordinal) else {
485            return false;
486        };
487        self.graph.find_edge(pred_idx, node_idx).is_some()
488    }
489
490    /// Attaches physical readout dependencies and rejects dependency cycles.
491    pub(crate) fn attach_readout_dependencies(
492        &mut self,
493        generators: &[StabilizerGenerator],
494        zx: Option<&crate::ZXGraph>,
495    ) -> Result<(), BlockGraphError> {
496        let fixings = selective_fixings(generators);
497        let no_fixings = Default::default();
498        let measurement_nodes = self
499            .ordered
500            .iter()
501            .filter_map(|&node| match &self.graph[node].action {
502                Action::Measure { name, .. } => Some((name.clone(), node)),
503                _ => None,
504            })
505            .collect::<BTreeMap<_, _>>();
506        let resolve_nodes = self
507            .ordered
508            .iter()
509            .filter_map(|&node| match self.graph[node].action {
510                Action::Resolve { target, .. } => Some((target, node)),
511                _ => None,
512            })
513            .collect::<HashMap<_, _>>();
514        let branch_nodes = self
515            .ordered
516            .iter()
517            .filter_map(|&node| match self.graph[node].action {
518                Action::Branch { target, .. } => Some((target, node)),
519                _ => None,
520            })
521            .collect::<HashMap<_, _>>();
522        let branch_regions = self
523            .branch_regions
524            .iter()
525            .filter_map(|region| {
526                branch_nodes
527                    .get(&region.target)
528                    .copied()
529                    .map(|node| (node, region.clone()))
530            })
531            .collect::<Vec<_>>();
532        let feedback_nodes = self
533            .ordered
534            .iter()
535            .filter_map(|&node| match &self.graph[node].action {
536                Action::Feedback { targets, .. } => Some((node, targets.clone())),
537                _ => None,
538            })
539            .collect::<Vec<_>>();
540
541        let surfaces = generators
542            .iter()
543            .filter_map(|generator| Some((generator, generator.named_readout()?)))
544            .flat_map(|(generator, recipe)| {
545                recipe.surfaces().map(move |surface| (generator, surface))
546            });
547        for (generator, surface) in surfaces {
548            let name = generator.measurement_name().expect("named readout");
549            let fixings = if generator.readout_plan().is_some() {
550                &no_fixings
551            } else {
552                &fixings
553            };
554            let Some(&measurement_node) = measurement_nodes.get(name) else {
555                continue;
556            };
557            self.graph[measurement_node].measurement_stabilizer = Some(surface.clone());
558
559            let selective_sites = reachable_selective_sites(surface, fixings);
560            for site in &selective_sites {
561                if let Some(&resolve_node) = resolve_nodes.get(site) {
562                    self.add_dependency(
563                        resolve_node,
564                        measurement_node,
565                        ActionDependency::SelectiveSupport,
566                    );
567                }
568            }
569            for (branch_node, region) in &branch_regions {
570                if measurement_touches_branch_region(surface, region, &selective_sites, fixings) {
571                    self.add_dependency(
572                        *branch_node,
573                        measurement_node,
574                        ActionDependency::BranchSupport,
575                    );
576                }
577            }
578            for (feedback_node, targets) in &feedback_nodes {
579                if feedback_perturbs_measurement(targets, surface, &selective_sites, fixings, zx) {
580                    self.add_dependency(
581                        *feedback_node,
582                        measurement_node,
583                        ActionDependency::FeedbackAnticommutation,
584                    );
585                }
586            }
587        }
588
589        for generator in generators {
590            if let Some(recipe) = generator.readout_plan() {
591                let name = generator.measurement_name().expect("named readout");
592                let measurement_node = measurement_nodes[name];
593                if generator.uses_source_outcomes() {
594                    for ordinal in recipe
595                        .branches
596                        .iter()
597                        .flat_map(|(_, branch)| &branch.readout_ordinals)
598                    {
599                        let predecessor = generators[*ordinal]
600                            .measurement_name()
601                            .expect("readout recipe references a named row");
602                        self.add_dependency(
603                            measurement_nodes[predecessor],
604                            measurement_node,
605                            ActionDependency::ReadoutParity,
606                        );
607                    }
608                }
609                self.graph[measurement_node].measurement_stabilizer =
610                    Some(recipe.branches[0].1.stabilizer.clone());
611                for site in &recipe.sites {
612                    if let Some(&resolve_node) = resolve_nodes.get(site) {
613                        self.add_dependency(
614                            resolve_node,
615                            measurement_node,
616                            ActionDependency::SelectiveSupport,
617                        );
618                    }
619                }
620            }
621        }
622
623        for (name, &node) in &measurement_nodes {
624            if self.graph[node].measurement_stabilizer.is_none() {
625                return Err(crate::StabilizerError::MeasurementSurfaceUnavailable {
626                    mvar: name.clone(),
627                }
628                .into());
629            }
630        }
631
632        self.validate_semantics()?;
633        self.analyzed = true;
634        Ok(())
635    }
636
637    fn add_dependency(
638        &mut self,
639        predecessor: NodeIndex<u32>,
640        consumer: NodeIndex<u32>,
641        dependency: ActionDependency,
642    ) {
643        if self
644            .graph
645            .edges_connecting(predecessor, consumer)
646            .any(|edge| *edge.weight() == dependency)
647        {
648            return;
649        }
650        self.graph.add_edge(predecessor, consumer, dependency);
651    }
652
653    fn validate_semantics(&self) -> Result<(), BlockGraphError> {
654        let mut defined_vars = self.inputs.iter().cloned().collect::<HashSet<_>>();
655        let mut resolve_targets = HashSet::new();
656        let mut branch_targets = HashSet::new();
657        let mut measurement_targets = HashSet::new();
658        let bucket_measurements = self
659            .ordered_nodes()
660            .filter_map(|node| match &node.action {
661                Action::Measure { name, .. } => Some(name.clone()),
662                _ => None,
663            })
664            .collect::<HashSet<_>>();
665
666        for action in self.ordered_nodes().map(|node| &node.action) {
667            BlockGraph::validate_action_syntax(action)?;
668            match action {
669                Action::Measure { target, name } => {
670                    if defined_vars.contains(name) {
671                        return Err(InvalidActionError::VariableRedefinition(name.clone()).into());
672                    }
673                    let site_key = measurement_site_key(target)?;
674                    if !measurement_targets.insert(site_key) {
675                        return Err(InvalidActionError::DuplicateMeasurementTarget(*target).into());
676                    }
677                    defined_vars.insert(name.clone());
678                }
679                Action::Let { name, expr } => {
680                    if defined_vars.contains(name) {
681                        return Err(InvalidActionError::VariableRedefinition(name.clone()).into());
682                    }
683                    check_expr_vars(expr, &defined_vars, &bucket_measurements)?;
684                    defined_vars.insert(name.clone());
685                }
686                Action::DiscardIf(expr) => {
687                    check_expr_vars(expr, &defined_vars, &bucket_measurements)?;
688                }
689                Action::Resolve { target, condition } => {
690                    check_expr_vars(condition, &defined_vars, &bucket_measurements)?;
691                    if !resolve_targets.insert(*target) {
692                        return Err(InvalidActionError::DuplicateResolveTarget(*target).into());
693                    }
694                }
695                Action::Branch { target, condition } => {
696                    check_expr_vars(condition, &defined_vars, &bucket_measurements)?;
697                    if !branch_targets.insert(*target) {
698                        return Err(InvalidActionError::DuplicateBranchTarget(*target).into());
699                    }
700                }
701                Action::Feedback { condition, .. } => {
702                    if let Some(condition) = condition {
703                        check_expr_vars(condition, &defined_vars, &bucket_measurements)?;
704                    }
705                }
706            }
707        }
708
709        toposort(&self.graph, None).map_err(|cycle| {
710            let ordinal = self.graph[cycle.node_id()].ordinal;
711            BlockGraphError::InvalidAction(InvalidActionError::DependencyCycle { ordinal })
712        })?;
713
714        Ok(())
715    }
716
717    fn validate_graph_constraints(&mut self, graph: &BlockGraph) -> Result<(), BlockGraphError> {
718        let mut selective_positions = graph
719            .blocks()
720            .filter(|block| block.kind.is_selective())
721            .map(|block| block.pos)
722            .collect::<HashSet<_>>();
723
724        for action in self.ordered_nodes().map(|node| &node.action) {
725            match action {
726                Action::Measure { target, .. } => {
727                    ensure_measure_target_is_valid(graph, target)?;
728                }
729                Action::Resolve { target, .. } => {
730                    match graph.get_block(*target) {
731                        Some(b) if b.kind.is_selective() => {}
732                        _ => {
733                            return Err(InvalidActionError::InvalidResolveTarget(*target).into());
734                        }
735                    }
736                    selective_positions.remove(target);
737                }
738                Action::Branch { target, .. } => {
739                    if graph.branch_by_target(*target).is_none() {
740                        return Err(InvalidActionError::InvalidBranchTarget(*target).into());
741                    }
742                }
743                Action::Feedback { targets, .. } => {
744                    for feedback in targets {
745                        ensure_feedback_target_exists(graph, feedback.target)?;
746                        if let Some(dir) = feedback.direction {
747                            let dst = crate::checked_add_position(feedback.target, dir.to_ivec3())?;
748                            if graph.get_pipe(feedback.target, dst).is_none() {
749                                return Err(InvalidActionError::InvalidFeedbackTarget(
750                                    feedback.target,
751                                )
752                                .into());
753                            }
754                        }
755                    }
756                }
757                Action::Let { .. } | Action::DiscardIf(..) => {}
758            }
759        }
760
761        if let Some(&missing) = selective_positions.iter().min_by_key(|pos| pos.to_array()) {
762            return Err(InvalidActionError::MissingResolveForSelective { target: missing }.into());
763        }
764
765        let actions = self
766            .ordered_nodes()
767            .map(|node| node.action.clone())
768            .collect::<Vec<_>>();
769        self.branch_regions = crate::branch::validate_regions_for_actions(graph, &actions)?;
770
771        Ok(())
772    }
773
774    pub(crate) fn validate(
775        &mut self,
776        source_graph: Option<&BlockGraph>,
777    ) -> Result<(), BlockGraphError> {
778        self.validate_semantics()?;
779
780        if let Some(graph) = source_graph {
781            self.validate_graph_constraints(graph)?;
782        }
783
784        Ok(())
785    }
786
787    /// Builds the exact distinct resolve-value domain at `targets`, following
788    /// `Let` bindings. A reduced shared decision diagram avoids enumerating the
789    /// measurement inputs; materializing the output domain can still be
790    /// exponential when that many distinct tuples are genuinely reachable.
791    /// A missing or cyclic resolve target has an empty domain.
792    ///
793    /// # Errors
794    ///
795    /// Returns an error if Boolean construction or enumeration exceeds its budget.
796    pub fn resolve_value_domain(
797        &self,
798        targets: &[IVec3],
799    ) -> Result<ResolveValueDomain, BooleanResourceError> {
800        let Some(mut diagram) = self.resolve_decision_diagram(targets)? else {
801            return Ok(ResolveValueDomain { values: Vec::new() });
802        };
803        Ok(ResolveValueDomain {
804            values: diagram.values()?,
805        })
806    }
807
808    /// Builds the exact resolve-value domain when it contains at most `limit`
809    /// distinct tuples.
810    ///
811    /// # Errors
812    ///
813    /// Returns Boolean resource exhaustion or the first observed size above
814    /// `limit` instead of truncating the domain.
815    pub fn resolve_value_domain_bounded(
816        &self,
817        targets: &[IVec3],
818        limit: usize,
819    ) -> Result<ResolveValueDomain, ResolveDomainError> {
820        self.resolve_value_domain_bounded_with_limits(targets, limit, BooleanLimits::DEFAULT)
821    }
822
823    /// Query a bounded output domain with an explicit Boolean work/allocation budget.
824    ///
825    /// # Errors
826    ///
827    /// Returns resource exhaustion or the first domain size above `limit`.
828    pub fn resolve_value_domain_bounded_with_limits(
829        &self,
830        targets: &[IVec3],
831        limit: usize,
832        boolean_limits: BooleanLimits,
833    ) -> Result<ResolveValueDomain, ResolveDomainError> {
834        let Some(mut diagram) =
835            self.resolve_decision_diagram_with_limits(targets, boolean_limits)?
836        else {
837            return Ok(ResolveValueDomain { values: Vec::new() });
838        };
839        let values = diagram.values_up_to(limit.saturating_add(1))?;
840        if values.len() > limit {
841            Err(ResolveDomainError::TooManyValues {
842                observed: values.len(),
843            })
844        } else {
845            Ok(ResolveValueDomain { values })
846        }
847    }
848
849    pub(crate) fn first_resolve_values_with_limits(
850        &self,
851        targets: &[IVec3],
852        limits: BooleanLimits,
853    ) -> Result<Option<Vec<bool>>, BooleanResourceError> {
854        let Some(mut diagram) = self.resolve_decision_diagram_with_limits(targets, limits)? else {
855            return Ok(None);
856        };
857        Ok(diagram.values_up_to(1)?.pop())
858    }
859
860    pub(crate) fn complete_resolve_values_with_limits(
861        &self,
862        targets: &[IVec3],
863        fixed: &[(IVec3, bool)],
864        limits: BooleanLimits,
865    ) -> Result<Option<Vec<bool>>, BooleanResourceError> {
866        let Some(mut diagram) = self.resolve_decision_diagram_with_limits(targets, limits)? else {
867            return Ok(None);
868        };
869        let mut conjunction = DECISION_TRUE;
870        for &(site, value) in fixed {
871            let Some(index) = targets.iter().position(|&target| target == site) else {
872                return Ok(None);
873            };
874            let root = diagram.roots[index];
875            let required = if value { root } else { diagram.negate(root)? };
876            conjunction = diagram.apply(crate::BinaryOp::And, conjunction, required)?;
877        }
878        if conjunction == DECISION_FALSE {
879            return Ok(None);
880        }
881        let mut values = Vec::new();
882        for index in 0..targets.len() {
883            let root = diagram.roots[index];
884            let false_root = diagram.negate(root)?;
885            let when_false = diagram.apply(crate::BinaryOp::And, conjunction, false_root)?;
886            let value = when_false == DECISION_FALSE;
887            conjunction = if value {
888                diagram.apply(crate::BinaryOp::And, conjunction, root)?
889            } else {
890                when_false
891            };
892            values.push(value);
893        }
894        Ok(Some(values))
895    }
896
897    /// Whether `values` is jointly reachable for `targets`, decided
898    /// symbolically without materializing the full output domain. Missing or
899    /// cyclic targets and mismatched slice lengths are not reachable.
900    ///
901    /// # Errors
902    ///
903    /// Returns an error if Boolean construction or evaluation exceeds its budget.
904    pub fn resolve_values_are_reachable(
905        &self,
906        targets: &[IVec3],
907        values: &[bool],
908    ) -> Result<bool, BooleanResourceError> {
909        let Some(mut diagram) = self.resolve_decision_diagram(targets)? else {
910            return Ok(false);
911        };
912        diagram.contains(values)
913    }
914
915    pub(crate) fn resolve_xor_is_with_limits(
916        &self,
917        targets: &[IVec3],
918        expected: bool,
919        limits: BooleanLimits,
920    ) -> Result<bool, BooleanResourceError> {
921        let Some(mut diagram) = self.resolve_decision_diagram_with_limits(targets, limits)? else {
922            return Ok(false);
923        };
924        let roots = std::mem::take(&mut diagram.roots);
925        let parity = roots.into_iter().try_fold(DECISION_FALSE, |parity, root| {
926            diagram.apply(crate::BinaryOp::Xor, parity, root)
927        })?;
928        Ok(parity
929            == if expected {
930                DECISION_TRUE
931            } else {
932                DECISION_FALSE
933            })
934    }
935
936    pub(crate) fn branch_values_up_to(
937        &self,
938        targets: &[IVec3],
939        limit: usize,
940    ) -> Result<Vec<Vec<bool>>, BooleanResourceError> {
941        self.branch_values_up_to_with_limits(targets, limit, BooleanLimits::DEFAULT)
942    }
943
944    pub(crate) fn branch_values_up_to_with_limits(
945        &self,
946        targets: &[IVec3],
947        limit: usize,
948        limits: BooleanLimits,
949    ) -> Result<Vec<Vec<bool>>, BooleanResourceError> {
950        let Some(mut diagram) = self.branch_decision_diagram(targets, limits)? else {
951            return Ok(Vec::new());
952        };
953        diagram.values_up_to(limit)
954    }
955
956    pub(crate) fn resolve_decision_diagram(
957        &self,
958        targets: &[IVec3],
959    ) -> Result<Option<ResolveDecisionDiagram>, BooleanResourceError> {
960        self.resolve_decision_diagram_with_limits(targets, BooleanLimits::DEFAULT)
961    }
962
963    pub(crate) fn resolve_decision_diagram_with_limits(
964        &self,
965        targets: &[IVec3],
966        limits: BooleanLimits,
967    ) -> Result<Option<ResolveDecisionDiagram>, BooleanResourceError> {
968        let exprs = targets
969            .iter()
970            .map(|&target| self.resolve_expr(target))
971            .collect::<Option<Vec<_>>>();
972        match exprs {
973            Some(exprs) => ResolveDecisionDiagram::build(&exprs, &self.let_bindings(), limits),
974            None => Ok(None),
975        }
976    }
977
978    fn branch_decision_diagram(
979        &self,
980        targets: &[IVec3],
981        limits: BooleanLimits,
982    ) -> Result<Option<ResolveDecisionDiagram>, BooleanResourceError> {
983        let exprs = targets
984            .iter()
985            .map(|&target| self.branch_expr(target))
986            .collect::<Option<Vec<_>>>();
987        match exprs {
988            Some(exprs) => ResolveDecisionDiagram::build(&exprs, &self.let_bindings(), limits),
989            None => Ok(None),
990        }
991    }
992
993    /// Sample one shared assignment of the measurement variables feeding the
994    /// requested resolves, then evaluate every available target condition from
995    /// that assignment. Targets without a resolve are omitted so structural
996    /// graph helpers can retain their independent fallback behavior.
997    pub(crate) fn sample_resolve_values(
998        &self,
999        targets: &[IVec3],
1000        rng: &mut impl Rng,
1001    ) -> HashMap<IVec3, bool> {
1002        let exprs = targets
1003            .iter()
1004            .filter_map(|&target| {
1005                self.ordered_nodes()
1006                    .find_map(|node| match &node.action {
1007                        Action::Resolve {
1008                            target: candidate,
1009                            condition,
1010                        } if *candidate == target => Some(condition),
1011                        _ => None,
1012                    })
1013                    .map(|expr| (target, expr))
1014            })
1015            .collect::<Vec<_>>();
1016        let aliases = self.let_bindings();
1017        let mut variables = BTreeSet::new();
1018        let mut visited = HashSet::new();
1019        for (_, expr) in &exprs {
1020            expand_expr_vars_to_measurements(expr, &aliases, &mut variables, &mut visited);
1021        }
1022        let values = variables
1023            .into_iter()
1024            .map(|name| (name, rng.random()))
1025            .collect::<HashMap<_, _>>();
1026
1027        let mut alias_values = HashMap::new();
1028        exprs
1029            .into_iter()
1030            .map(|(target, expr)| {
1031                (
1032                    target,
1033                    evaluate_expr_with(expr, &aliases, &|name| values[name], &mut alias_values),
1034                )
1035            })
1036            .collect()
1037    }
1038
1039    fn resolve_expr(&self, target: IVec3) -> Option<&Expr> {
1040        self.ordered_nodes().find_map(|node| match &node.action {
1041            Action::Resolve {
1042                target: candidate,
1043                condition,
1044            } if *candidate == target => Some(condition),
1045            _ => None,
1046        })
1047    }
1048
1049    fn branch_expr(&self, target: IVec3) -> Option<&Expr> {
1050        self.ordered_nodes().find_map(|node| match &node.action {
1051            Action::Branch {
1052                target: candidate,
1053                condition,
1054            } if *candidate == target => Some(condition),
1055            _ => None,
1056        })
1057    }
1058
1059    fn let_bindings(&self) -> HashMap<&str, &Expr> {
1060        self.ordered_nodes()
1061            .filter_map(|node| match &node.action {
1062                Action::Let { name, expr } => Some((name.as_str(), expr)),
1063                _ => None,
1064            })
1065            .collect()
1066    }
1067}
1068
1069fn reachable_selective_sites(
1070    row: &Stabilizer,
1071    fixings: &crate::SelectiveFixings<'_>,
1072) -> Vec<IVec3> {
1073    let mut sites = fixings
1074        .keys()
1075        .copied()
1076        .filter(|site| {
1077            row.interior_nodes
1078                .get(site)
1079                .is_some_and(|pauli| *pauli != crate::Pauli::I)
1080        })
1081        .collect::<HashSet<_>>();
1082    let mut pending = sites.iter().copied().collect::<Vec<_>>();
1083    while let Some(site) = pending.pop() {
1084        for (&pos, &pauli) in &fixings[&site].row.interior_nodes {
1085            if pauli != crate::Pauli::I && fixings.contains_key(&pos) && sites.insert(pos) {
1086                pending.push(pos);
1087            }
1088        }
1089    }
1090    let mut sites = sites.into_iter().collect::<Vec<_>>();
1091    sites.sort_unstable_by_key(|pos| (pos.x, pos.y, pos.z));
1092    sites
1093}
1094
1095fn measurement_touches_branch_region(
1096    row: &Stabilizer,
1097    region: &BranchRegion,
1098    selective_sites: &[IVec3],
1099    fixings: &crate::SelectiveFixings<'_>,
1100) -> bool {
1101    stabilizer_touches_branch_region(row, region)
1102        || selective_sites
1103            .iter()
1104            .any(|site| stabilizer_touches_branch_region(fixings[site].row, region))
1105}
1106
1107fn stabilizer_touches_branch_region(row: &Stabilizer, region: &BranchRegion) -> bool {
1108    // ZX materialization records block anchors even for extended blocks;
1109    // `BranchRegion::blocks` uses that same coordinate keyspace.
1110    row.interior_nodes
1111        .iter()
1112        .any(|(&position, &pauli)| pauli != crate::Pauli::I && region.contains_block(position))
1113        || row.interior_edges.iter().any(|(&(first, second), &pauli)| {
1114            pauli != crate::Pauli::I
1115                && (region.contains_block(first) || region.contains_block(second))
1116        })
1117}
1118
1119fn feedback_perturbs_measurement(
1120    targets: &[FeedbackTarget],
1121    row: &Stabilizer,
1122    selective_sites: &[IVec3],
1123    fixings: &crate::SelectiveFixings<'_>,
1124    zx: Option<&crate::ZXGraph>,
1125) -> bool {
1126    row.odd_anticommutes_feedback(targets, zx)
1127        || selective_sites
1128            .iter()
1129            .any(|site| fixings[site].row.odd_anticommutes_feedback(targets, zx))
1130}
1131
1132fn evaluate_expr_with(
1133    expr: &Expr,
1134    aliases: &HashMap<&str, &Expr>,
1135    value_of: &impl Fn(&str) -> bool,
1136    alias_values: &mut HashMap<String, bool>,
1137) -> bool {
1138    match expr {
1139        Expr::Var(name) => {
1140            if let Some(&value) = alias_values.get(name) {
1141                return value;
1142            }
1143            let Some(expr) = aliases.get(name.as_str()) else {
1144                return value_of(name);
1145            };
1146            let value = evaluate_expr_with(expr, aliases, value_of, alias_values);
1147            alias_values.insert(name.clone(), value);
1148            value
1149        }
1150        Expr::Not(expr) => !evaluate_expr_with(expr, aliases, value_of, alias_values),
1151        Expr::Binary(op, left, right) => {
1152            let left = evaluate_expr_with(left, aliases, value_of, alias_values);
1153            let right = evaluate_expr_with(right, aliases, value_of, alias_values);
1154            match op {
1155                crate::BinaryOp::Xor => left ^ right,
1156                crate::BinaryOp::And => left & right,
1157                crate::BinaryOp::Or => left | right,
1158            }
1159        }
1160    }
1161}
1162
1163fn defines_name(action: &Action) -> Option<&str> {
1164    match action {
1165        Action::Let { name, .. } | Action::Measure { name, .. } => Some(name),
1166        Action::DiscardIf(_)
1167        | Action::Resolve { .. }
1168        | Action::Branch { .. }
1169        | Action::Feedback { .. } => None,
1170    }
1171}
1172
1173fn expand_expr_vars_to_measurements(
1174    expr: &Expr,
1175    aliases: &HashMap<&str, &Expr>,
1176    out: &mut BTreeSet<String>,
1177    visited: &mut HashSet<String>,
1178) {
1179    match expr {
1180        Expr::Var(name) => {
1181            if let Some(resolved) = aliases.get(name.as_str()) {
1182                if !visited.insert(name.clone()) {
1183                    return;
1184                }
1185                expand_expr_vars_to_measurements(resolved, aliases, out, visited);
1186            } else {
1187                out.insert(name.clone());
1188            }
1189        }
1190        Expr::Not(expr) => expand_expr_vars_to_measurements(expr, aliases, out, visited),
1191        Expr::Binary(_, lhs, rhs) => {
1192            expand_expr_vars_to_measurements(lhs, aliases, out, visited);
1193            expand_expr_vars_to_measurements(rhs, aliases, out, visited);
1194        }
1195    }
1196}
1197
1198fn check_expr_vars(
1199    expr: &Expr,
1200    defined: &HashSet<String>,
1201    bucket_measurements: &HashSet<String>,
1202) -> Result<(), InvalidActionError> {
1203    match expr {
1204        Expr::Var(name) => {
1205            if !defined.contains(name) && !bucket_measurements.contains(name) {
1206                return Err(InvalidActionError::UndefinedVariable(name.clone()));
1207            }
1208        }
1209        Expr::Not(expr) => check_expr_vars(expr, defined, bucket_measurements)?,
1210        Expr::Binary(_, lhs, rhs) => {
1211            check_expr_vars(lhs, defined, bucket_measurements)?;
1212            check_expr_vars(rhs, defined, bucket_measurements)?;
1213        }
1214    }
1215    Ok(())
1216}
1217
1218#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1219enum MeasurementSiteKey {
1220    Node(IVec3),
1221    Edge { a: IVec3, b: IVec3 },
1222}
1223
1224fn measurement_site_key(target: &MeasureTarget) -> Result<MeasurementSiteKey, BlockGraphError> {
1225    Ok(match target {
1226        MeasureTarget::Node(pos) => MeasurementSiteKey::Node(*pos),
1227        MeasureTarget::Edge { src, dir } => {
1228            let dst = crate::checked_add_position(*src, dir.to_ivec3())?;
1229            if src.to_array() <= dst.to_array() {
1230                MeasurementSiteKey::Edge { a: *src, b: dst }
1231            } else {
1232                MeasurementSiteKey::Edge { a: dst, b: *src }
1233            }
1234        }
1235    })
1236}
1237
1238fn ensure_measure_target_is_valid(
1239    graph: &BlockGraph,
1240    target: &MeasureTarget,
1241) -> Result<(), InvalidActionError> {
1242    match target {
1243        MeasureTarget::Node(pos) => match graph.get_block(*pos).map(|block| block.kind) {
1244            Some(
1245                BlockKind::Cube(_)
1246                | BlockKind::Y
1247                | BlockKind::Measurement(_)
1248                | BlockKind::Selective(_),
1249            ) => Ok(()),
1250            _ => Err(InvalidActionError::InvalidMeasurementNode(*pos)),
1251        },
1252        MeasureTarget::Edge { src, dir } if dir.as_udirection() == crate::UDirection::Z => {
1253            Err(InvalidActionError::TimeLikeMeasurementEdge(*src, *dir))
1254        }
1255        MeasureTarget::Edge { .. } => Ok(()),
1256    }
1257}
1258
1259fn ensure_feedback_target_exists(graph: &BlockGraph, pos: IVec3) -> Result<(), InvalidActionError> {
1260    if graph.get_block(pos).is_none() {
1261        return Err(InvalidActionError::InvalidFeedbackTarget(pos));
1262    }
1263    Ok(())
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268    use super::{ResolveDomainError, SetMeasurementObservableError};
1269    use bloq_utils::boolean::{BooleanLimits, BooleanResourceError};
1270    use glam::ivec3;
1271
1272    use crate::validate::InvalidActionError;
1273    use crate::{
1274        Action, Block, BlockGraph, BlockKind, CubeKind, Direction, Expr, MeasureTarget, Pipe,
1275    };
1276    use crate::{
1277        ActionDag, ActionDependency, BlockGraphError, MeasurementObservable, PauliBasis,
1278        SelectiveKind,
1279    };
1280
1281    #[test]
1282    fn shared_aliases_stay_bounded_for_domain_and_sampling() {
1283        let target = ivec3(0, 0, 1);
1284        let mut actions = vec![Action::Measure {
1285            target: MeasureTarget::Node(ivec3(0, 0, 0)),
1286            name: "m".into(),
1287        }];
1288        let mut name = "m".to_owned();
1289        for index in 0..64 {
1290            let next = format!("a{index}");
1291            actions.push(Action::Let {
1292                name: next.clone(),
1293                expr: Expr::Binary(
1294                    crate::BinaryOp::Xor,
1295                    Box::new(Expr::Var(name.clone())),
1296                    Box::new(Expr::Var(name)),
1297                ),
1298            });
1299            name = next;
1300        }
1301        actions.push(Action::Resolve {
1302            target,
1303            condition: Expr::Var(name),
1304        });
1305        let dag = ActionDag::from_actions(&actions);
1306        assert_eq!(
1307            dag.resolve_value_domain(&[target]).unwrap().values(),
1308            &[vec![false]]
1309        );
1310        assert!(!dag.sample_resolve_values(&[target], &mut rand::rng())[&target]);
1311    }
1312
1313    #[test]
1314    fn resolve_domain_preserves_shared_control_correlation() {
1315        let first = ivec3(0, 0, 1);
1316        let second = ivec3(1, 0, 1);
1317        let dag = ActionDag::from_actions(&[
1318            Action::Measure {
1319                target: MeasureTarget::Node(ivec3(0, 0, 0)),
1320                name: "m".into(),
1321            },
1322            Action::Let {
1323                name: "not_m".into(),
1324                expr: Expr::Not(Box::new(Expr::Var("m".into()))),
1325            },
1326            Action::Resolve {
1327                target: first,
1328                condition: Expr::Var("m".into()),
1329            },
1330            Action::Resolve {
1331                target: second,
1332                condition: Expr::Var("not_m".into()),
1333            },
1334        ]);
1335
1336        let domain = dag.resolve_value_domain(&[first, second]).unwrap();
1337        assert_eq!(domain.values(), &[vec![false, true], vec![true, false]]);
1338        assert!(
1339            dag.resolve_xor_is_with_limits(&[first, second], true, BooleanLimits::DEFAULT)
1340                .unwrap()
1341        );
1342    }
1343
1344    #[test]
1345    fn branch_domain_preserves_shared_control_correlation() {
1346        let first = ivec3(0, 0, 1);
1347        let second = ivec3(1, 0, 1);
1348        let dag = ActionDag::from_actions(&[
1349            Action::Measure {
1350                target: MeasureTarget::Node(ivec3(0, 0, 0)),
1351                name: "m".into(),
1352            },
1353            Action::Let {
1354                name: "not_m".into(),
1355                expr: Expr::Not(Box::new(Expr::Var("m".into()))),
1356            },
1357            Action::Branch {
1358                target: first,
1359                condition: Expr::Var("m".into()),
1360            },
1361            Action::Branch {
1362                target: second,
1363                condition: Expr::Var("not_m".into()),
1364            },
1365        ]);
1366
1367        // The shared control makes the branch domain the two anti-correlated
1368        // tuples only; (false, false) must not appear.
1369        let domain = dag
1370            .branch_values_up_to(&[first, second], usize::MAX)
1371            .unwrap();
1372        assert_eq!(domain, [vec![false, true], vec![true, false]]);
1373    }
1374
1375    #[test]
1376    fn symbolic_domain_matches_a_small_truth_table() {
1377        let targets = [ivec3(0, 0, 1), ivec3(1, 0, 1), ivec3(2, 0, 1)];
1378        let actions = [
1379            Action::Let {
1380                name: "parity".into(),
1381                expr: Expr::Binary(
1382                    crate::BinaryOp::Xor,
1383                    Box::new(Expr::Var("a".into())),
1384                    Box::new(Expr::Var("b".into())),
1385                ),
1386            },
1387            Action::Resolve {
1388                target: targets[0],
1389                condition: Expr::Var("parity".into()),
1390            },
1391            Action::Resolve {
1392                target: targets[1],
1393                condition: Expr::Binary(
1394                    crate::BinaryOp::And,
1395                    Box::new(Expr::Var("parity".into())),
1396                    Box::new(Expr::Var("c".into())),
1397                ),
1398            },
1399            Action::Resolve {
1400                target: targets[2],
1401                condition: Expr::Binary(
1402                    crate::BinaryOp::Or,
1403                    Box::new(Expr::Not(Box::new(Expr::Var("a".into())))),
1404                    Box::new(Expr::Var("c".into())),
1405                ),
1406            },
1407        ];
1408        let dag = ActionDag::from_actions(&actions);
1409        let aliases = dag.let_bindings();
1410        let exprs = targets.map(|target| dag.resolve_expr(target).unwrap());
1411        let mut expected = std::collections::BTreeSet::new();
1412        for assignment in 0..8 {
1413            expected.insert(
1414                exprs
1415                    .iter()
1416                    .map(|expr| {
1417                        super::evaluate_expr_with(
1418                            expr,
1419                            &aliases,
1420                            &|name| {
1421                                let index = match name {
1422                                    "a" => 0,
1423                                    "b" => 1,
1424                                    "c" => 2,
1425                                    _ => unreachable!("all input variables are listed"),
1426                                };
1427                                assignment >> index & 1 == 1
1428                            },
1429                            &mut std::collections::HashMap::new(),
1430                        )
1431                    })
1432                    .collect::<Vec<_>>(),
1433            );
1434        }
1435
1436        let expected = expected.into_iter().collect::<Vec<_>>();
1437        let domain = dag.resolve_value_domain(&targets).unwrap();
1438        assert_eq!(domain.values(), expected);
1439        let bounded = dag
1440            .resolve_value_domain_bounded(&targets, expected.len())
1441            .expect("the exact domain fits its measured size");
1442        assert_eq!(bounded.values(), expected);
1443        assert_eq!(
1444            dag.resolve_value_domain_bounded(&targets, expected.len() - 1),
1445            Err(ResolveDomainError::TooManyValues {
1446                observed: expected.len()
1447            })
1448        );
1449        for assignment in 0..8 {
1450            let values = (0..3)
1451                .map(|index| assignment >> index & 1 == 1)
1452                .collect::<Vec<_>>();
1453            assert_eq!(
1454                dag.resolve_values_are_reachable(&targets, &values).unwrap(),
1455                domain.contains(&values)
1456            );
1457        }
1458    }
1459
1460    #[test]
1461    fn domain_limits_distinguish_alias_work_from_value_count() {
1462        let target = ivec3(0, 0, 1);
1463        let mut actions = (0..256)
1464            .map(|index| Action::Let {
1465                name: format!("a{index}"),
1466                expr: Expr::Var(if index == 0 {
1467                    "input".into()
1468                } else {
1469                    format!("a{}", index - 1)
1470                }),
1471            })
1472            .collect::<Vec<_>>();
1473        actions.push(Action::Resolve {
1474            target,
1475            condition: Expr::Var("a255".into()),
1476        });
1477        let dag = ActionDag::from_actions(&actions);
1478        let query = |values, limits| {
1479            dag.resolve_value_domain_bounded_with_limits(&[target], values, limits)
1480        };
1481        assert!(matches!(
1482            query(
1483                2,
1484                BooleanLimits {
1485                    max_steps: 8,
1486                    ..BooleanLimits::DEFAULT
1487                }
1488            ),
1489            Err(ResolveDomainError::Resource(BooleanResourceError {
1490                resource: "Boolean work steps",
1491                observed: 9,
1492                limit: 8,
1493            }))
1494        ));
1495        assert!(matches!(
1496            query(
1497                2,
1498                BooleanLimits {
1499                    max_nodes: 0,
1500                    ..BooleanLimits::DEFAULT
1501                }
1502            ),
1503            Err(ResolveDomainError::Resource(BooleanResourceError {
1504                resource: "Boolean nodes",
1505                observed: 1,
1506                limit: 0,
1507            }))
1508        ));
1509        assert_eq!(
1510            query(1, BooleanLimits::DEFAULT),
1511            Err(ResolveDomainError::TooManyValues { observed: 2 })
1512        );
1513        let domain = query(2, BooleanLimits::DEFAULT).unwrap();
1514        assert_eq!(domain.values(), &[vec![false], vec![true]]);
1515        assert_eq!(domain, query(2, BooleanLimits::UNLIMITED).unwrap());
1516        assert_eq!(
1517            dag.complete_resolve_values_with_limits(
1518                &[target],
1519                &[(target, true)],
1520                BooleanLimits::DEFAULT
1521            )
1522            .unwrap(),
1523            Some(vec![true])
1524        );
1525    }
1526
1527    #[test]
1528    fn symbolic_reachability_handles_wide_independent_controls() {
1529        let count = usize::BITS as usize + 1;
1530        let targets = (0..count)
1531            .map(|index| ivec3(index as i32, 0, 0))
1532            .collect::<Vec<_>>();
1533        let actions = targets
1534            .iter()
1535            .enumerate()
1536            .map(|(index, &target)| Action::Resolve {
1537                target,
1538                condition: Expr::Var(format!("m{index}")),
1539            })
1540            .collect::<Vec<_>>();
1541        let dag = ActionDag::from_actions(&actions);
1542
1543        assert!(
1544            dag.resolve_values_are_reachable(&targets, &vec![false; count])
1545                .unwrap()
1546        );
1547    }
1548
1549    #[test]
1550    fn measure_target_with_shift_updates_node_and_edge_geometry() {
1551        use crate::{Action, Direction, MeasureTarget};
1552        use glam::ivec3;
1553
1554        let shifted_node = Action::Measure {
1555            target: MeasureTarget::Node(ivec3(0, 0, 0)),
1556            name: "m_node".into(),
1557        }
1558        .with_shift(ivec3(2, 3, 4));
1559        assert_eq!(
1560            shifted_node,
1561            Action::Measure {
1562                target: MeasureTarget::Node(ivec3(2, 3, 4)),
1563                name: "m_node".into(),
1564            }
1565        );
1566
1567        let shifted_edge = Action::Measure {
1568            target: MeasureTarget::Edge {
1569                src: ivec3(1, 0, 0),
1570                dir: Direction::XPLUS,
1571            },
1572            name: "m_edge".into(),
1573        }
1574        .with_shift(ivec3(0, 5, -1));
1575        assert_eq!(
1576            shifted_edge,
1577            Action::Measure {
1578                target: MeasureTarget::Edge {
1579                    src: ivec3(1, 5, -1),
1580                    dir: Direction::XPLUS,
1581                },
1582                name: "m_edge".into(),
1583            }
1584        );
1585    }
1586
1587    #[test]
1588    fn measurement_metadata_requires_an_existing_measurement() {
1589        use crate::{Action, ActionDag, MeasureTarget};
1590        use glam::ivec3;
1591
1592        let mut dag = ActionDag::from_actions(&[Action::Measure {
1593            target: MeasureTarget::Node(ivec3(0, 0, 0)),
1594            name: "m0".into(),
1595        }]);
1596        let observable = MeasurementObservable::Selective(SelectiveKind::XY);
1597        dag.set_measurement_observable(0, observable).unwrap();
1598        assert_eq!(
1599            dag.node_by_ordinal(0).unwrap().measurement,
1600            Some(observable)
1601        );
1602        assert!(matches!(
1603            dag.set_measurement_observable(1, observable),
1604            Err(SetMeasurementObservableError::OrdinalOutOfRange { ordinal: 1, .. })
1605        ));
1606
1607        let mut dag = ActionDag::from_actions(&[Action::Let {
1608            name: "a".into(),
1609            expr: Expr::Var("m0".into()),
1610        }]);
1611        assert!(matches!(
1612            dag.set_measurement_observable(0, MeasurementObservable::Concrete(PauliBasis::Z)),
1613            Err(SetMeasurementObservableError::NotMeasurementNode { ordinal: 0 })
1614        ));
1615    }
1616
1617    #[test]
1618    fn action_dag_adds_dependency_edges_for_variable_uses() {
1619        let mut graph = BlockGraph::new();
1620        graph.add_block(Block::new(
1621            glam::ivec3(0, 0, 0),
1622            BlockKind::Cube(CubeKind::ZXZ),
1623        ));
1624        graph
1625            .add_action(Action::Measure {
1626                target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1627                name: "m0".into(),
1628            })
1629            .unwrap();
1630        graph
1631            .add_action(Action::Let {
1632                name: "alias".into(),
1633                expr: Expr::Var("m0".into()),
1634            })
1635            .unwrap();
1636        graph
1637            .add_action(Action::DiscardIf(Expr::Var("alias".into())))
1638            .unwrap();
1639
1640        let dag = graph.action_graph();
1641        let ordinals = dag
1642            .ordered_nodes()
1643            .map(|node| node.ordinal)
1644            .collect::<Vec<_>>();
1645        assert_eq!(ordinals, vec![0, 1, 2]);
1646
1647        let alias_node = dag.node_by_ordinal(1).unwrap();
1648        let discard_node = dag.node_by_ordinal(2).unwrap();
1649        assert!(dag.depends_on(alias_node, 0));
1650        assert!(dag.depends_on(discard_node, 1));
1651    }
1652
1653    #[test]
1654    fn action_dag_requires_resolve_before_selective_measurement() {
1655        let graph = BlockGraph::from_blog_text(
1656            "BLOG 1.0\n\n  0: Port [0,0,0]\n  1: XZX [0,0,1]\n  2: XY [0,0,2]\n  3: ZXZ [1,0,1]\n  [0,0,0] -> +Z\n  [0,0,1] -> +Z\n  [0,0,1] -> +X\n\n  resolve 2 if m0\n  m_sel = measure 2\n  m0 = measure 1 -> +X\n",
1657        )
1658        .unwrap();
1659
1660        let dag = graph.action_graph();
1661        let resolve = dag
1662            .ordered_nodes()
1663            .find(|node| matches!(node.action, Action::Resolve { target, .. } if target == glam::ivec3(0, 0, 2)))
1664            .unwrap();
1665        let measure = dag
1666            .ordered_nodes()
1667            .find(|node| matches!(&node.action, Action::Measure { name, .. } if name == "m_sel"))
1668            .unwrap();
1669        assert!(dag.depends_on(measure, resolve.ordinal));
1670    }
1671
1672    #[test]
1673    fn action_dag_rejects_variable_redefinition() {
1674        let actions = vec![
1675            Action::Measure {
1676                target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1677                name: "m0".into(),
1678            },
1679            Action::Let {
1680                name: "a".into(),
1681                expr: Expr::Var("m0".into()),
1682            },
1683            Action::Let {
1684                name: "a".into(),
1685                expr: Expr::Var("m0".into()),
1686            },
1687        ];
1688
1689        let mut dag = ActionDag::from_actions(&actions);
1690        let err = dag.validate(None).unwrap_err();
1691        assert!(
1692            matches!(
1693                err,
1694                BlockGraphError::InvalidAction(InvalidActionError::VariableRedefinition(ref n))
1695                    if n == "a"
1696            ),
1697            "expected VariableRedefinition, got: {err:?}",
1698        );
1699    }
1700
1701    #[test]
1702    fn action_dag_rejects_duplicate_resolve_target() {
1703        let actions = vec![
1704            Action::Measure {
1705                target: MeasureTarget::Node(glam::ivec3(0, 0, -1)),
1706                name: "m0".into(),
1707            },
1708            Action::Resolve {
1709                target: glam::ivec3(0, 0, 0),
1710                condition: Expr::Var("m0".into()),
1711            },
1712            Action::Resolve {
1713                target: glam::ivec3(0, 0, 0),
1714                condition: Expr::Var("m0".into()),
1715            },
1716        ];
1717
1718        let mut dag = ActionDag::from_actions(&actions);
1719        let err = dag.validate(None).unwrap_err();
1720        assert!(
1721            matches!(
1722                err,
1723                BlockGraphError::InvalidAction(InvalidActionError::DuplicateResolveTarget(target))
1724                    if target == glam::ivec3(0, 0, 0)
1725            ),
1726            "expected DuplicateResolveTarget, got: {err:?}",
1727        );
1728    }
1729
1730    #[test]
1731    fn action_dag_rejects_duplicate_measurement_target() {
1732        let actions = vec![
1733            Action::Measure {
1734                target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1735                name: "m0".into(),
1736            },
1737            Action::Measure {
1738                target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1739                name: "m1".into(),
1740            },
1741        ];
1742
1743        let mut dag = ActionDag::from_actions(&actions);
1744        let err = dag.validate(None).unwrap_err();
1745        assert!(
1746            matches!(
1747                err,
1748                BlockGraphError::InvalidAction(InvalidActionError::DuplicateMeasurementTarget(..))
1749            ),
1750            "expected DuplicateMeasurementTarget, got: {err:?}",
1751        );
1752    }
1753
1754    #[test]
1755    fn action_dag_rejects_missing_resolve_for_selective_block() {
1756        let mut graph = BlockGraph::new();
1757        graph.add_block(Block::new(
1758            glam::ivec3(0, 0, 0),
1759            BlockKind::Selective(SelectiveKind::XY),
1760        ));
1761        graph.add_block(Block::new(
1762            glam::ivec3(0, 0, -1),
1763            BlockKind::Cube(CubeKind::ZXZ),
1764        ));
1765        graph.add_pipe(Pipe::new(glam::ivec3(0, 0, -1), Direction::ZPLUS));
1766
1767        let err = ActionDag::from_actions(&[])
1768            .validate(Some(&graph))
1769            .unwrap_err();
1770        assert!(
1771            matches!(
1772                err,
1773                BlockGraphError::InvalidAction(
1774                    InvalidActionError::MissingResolveForSelective { target }
1775                ) if target == glam::ivec3(0, 0, 0)
1776            ),
1777            "expected MissingResolveForSelective, got: {err:?}",
1778        );
1779    }
1780
1781    #[test]
1782    fn action_dag_rejects_dependency_cycle() {
1783        let actions = vec![
1784            Action::Measure {
1785                target: MeasureTarget::Node(glam::ivec3(0, 0, 0)),
1786                name: "m".into(),
1787            },
1788            Action::Resolve {
1789                target: glam::ivec3(0, 0, 0),
1790                condition: Expr::Var("m".into()),
1791            },
1792        ];
1793
1794        let mut dag = ActionDag::from_actions(&actions);
1795        let err = dag.validate(None).unwrap_err();
1796        match err {
1797            BlockGraphError::InvalidAction(InvalidActionError::DependencyCycle { ordinal }) => {
1798                assert!(ordinal == 0 || ordinal == 1, "got ordinal: {ordinal}");
1799            }
1800            other => panic!("expected DependencyCycle, got: {other:?}"),
1801        }
1802    }
1803
1804    #[test]
1805    fn stabilizer_analysis_adds_all_implicit_dependencies_and_surface() {
1806        use crate::{
1807            FeedbackTarget, SelectiveFixingTarget, Stabilizer, StabilizerGenerator,
1808            StabilizerRowKind,
1809        };
1810
1811        let selective_a = ivec3(1, 0, 0);
1812        let selective_b = ivec3(2, 0, 0);
1813        let feedback_target = ivec3(3, 0, 0);
1814        let actions = vec![
1815            Action::Measure {
1816                target: MeasureTarget::Node(ivec3(0, 0, 0)),
1817                name: "control".into(),
1818            },
1819            Action::Resolve {
1820                target: selective_a,
1821                condition: Expr::Var("control".into()),
1822            },
1823            Action::Resolve {
1824                target: selective_b,
1825                condition: Expr::Var("control".into()),
1826            },
1827            Action::Feedback {
1828                targets: vec![FeedbackTarget {
1829                    pauli: PauliBasis::X,
1830                    target: feedback_target,
1831                    direction: None,
1832                }],
1833                condition: None,
1834            },
1835            Action::Measure {
1836                target: MeasureTarget::Node(ivec3(4, 0, 0)),
1837                name: "m".into(),
1838            },
1839        ];
1840        let measurement = Stabilizer::from_interior_nodes([
1841            (selective_a, crate::Pauli::X),
1842            (feedback_target, crate::Pauli::Z),
1843        ]);
1844        let fixing_a = Stabilizer::from_interior_nodes([
1845            (selective_a, crate::Pauli::Y),
1846            (selective_b, crate::Pauli::Z),
1847        ]);
1848        let fixing_b = Stabilizer::from_interior_nodes([(selective_b, crate::Pauli::Y)]);
1849        let generators = vec![
1850            StabilizerGenerator::new(
1851                Stabilizer::from_interior_nodes([(ivec3(0, 0, 0), crate::Pauli::X)]),
1852                StabilizerRowKind::Measurement {
1853                    name: "control".into(),
1854                },
1855            ),
1856            StabilizerGenerator::new(
1857                measurement.clone(),
1858                StabilizerRowKind::Measurement { name: "m".into() },
1859            ),
1860            StabilizerGenerator::new(
1861                fixing_a,
1862                StabilizerRowKind::SelectiveFixing {
1863                    targets: vec![SelectiveFixingTarget {
1864                        pos: selective_a,
1865                        forbidden: crate::Pauli::Y,
1866                    }],
1867                },
1868            ),
1869            StabilizerGenerator::new(
1870                fixing_b,
1871                StabilizerRowKind::SelectiveFixing {
1872                    targets: vec![SelectiveFixingTarget {
1873                        pos: selective_b,
1874                        forbidden: crate::Pauli::Y,
1875                    }],
1876                },
1877            ),
1878        ];
1879
1880        let mut dag = ActionDag::from_actions(&actions);
1881        dag.attach_readout_dependencies(&generators, None).unwrap();
1882
1883        let dependencies = dag.dependencies().collect::<Vec<_>>();
1884        assert!(dependencies.contains(&(1, 4, ActionDependency::SelectiveSupport)));
1885        assert!(dependencies.contains(&(2, 4, ActionDependency::SelectiveSupport)));
1886        assert!(dependencies.contains(&(3, 4, ActionDependency::FeedbackAnticommutation)));
1887        assert_eq!(
1888            dag.node_by_ordinal(4).unwrap().measurement_stabilizer,
1889            Some(measurement)
1890        );
1891        assert!(dag.is_analyzed());
1892    }
1893
1894    #[test]
1895    fn implicit_feedback_dependency_rejects_a_cycle() {
1896        use crate::{FeedbackTarget, Stabilizer, StabilizerGenerator, StabilizerRowKind};
1897
1898        let target = ivec3(0, 0, 0);
1899        let actions = vec![
1900            Action::Measure {
1901                target: MeasureTarget::Node(target),
1902                name: "m".into(),
1903            },
1904            Action::Feedback {
1905                targets: vec![FeedbackTarget {
1906                    pauli: PauliBasis::X,
1907                    target,
1908                    direction: None,
1909                }],
1910                condition: Some(Expr::Var("m".into())),
1911            },
1912        ];
1913        let generators = vec![StabilizerGenerator::new(
1914            Stabilizer::from_interior_nodes([(target, crate::Pauli::Z)]),
1915            StabilizerRowKind::Measurement { name: "m".into() },
1916        )];
1917
1918        let err = ActionDag::from_actions(&actions)
1919            .attach_readout_dependencies(&generators, None)
1920            .unwrap_err();
1921        assert!(matches!(
1922            err,
1923            BlockGraphError::InvalidAction(InvalidActionError::DependencyCycle { .. })
1924        ));
1925    }
1926
1927    #[test]
1928    fn stabilizer_analysis_adds_terminal_branch_support_dependency() {
1929        use crate::{BranchRegion, Stabilizer, StabilizerGenerator, StabilizerRowKind};
1930
1931        let controller = ivec3(0, 0, 0);
1932        let branch_target = ivec3(0, 0, 1);
1933        let actions = [
1934            Action::Measure {
1935                target: MeasureTarget::Node(controller),
1936                name: "control".into(),
1937            },
1938            Action::Branch {
1939                target: branch_target,
1940                condition: Expr::Var("control".into()),
1941            },
1942            Action::Measure {
1943                target: MeasureTarget::Node(ivec3(1, 0, 0)),
1944                name: "m".into(),
1945            },
1946        ];
1947        let generators = [
1948            StabilizerGenerator::new(
1949                Stabilizer::from_interior_nodes([(controller, crate::Pauli::X)]),
1950                StabilizerRowKind::Measurement {
1951                    name: "control".into(),
1952                },
1953            ),
1954            StabilizerGenerator::new(
1955                Stabilizer::from_interior_nodes([])
1956                    .with_interior_edges([((controller, branch_target), crate::Pauli::Z)]),
1957                StabilizerRowKind::Measurement { name: "m".into() },
1958            ),
1959        ];
1960
1961        let mut dag = ActionDag::from_actions(&actions);
1962        dag.branch_regions.push(BranchRegion::test_region(
1963            branch_target,
1964            vec![branch_target],
1965            Vec::new(),
1966        ));
1967        dag.attach_readout_dependencies(&generators, None).unwrap();
1968
1969        assert!(
1970            dag.dependencies()
1971                .any(|dependency| dependency == (1, 2, ActionDependency::BranchSupport))
1972        );
1973    }
1974
1975    #[test]
1976    fn terminal_branch_support_rejects_self_control_cycle() {
1977        use crate::{BranchRegion, Stabilizer, StabilizerGenerator, StabilizerRowKind};
1978
1979        let controller = ivec3(0, 0, 0);
1980        let branch_target = ivec3(0, 0, 1);
1981        let actions = [
1982            Action::Measure {
1983                target: MeasureTarget::Node(controller),
1984                name: "control".into(),
1985            },
1986            Action::Branch {
1987                target: branch_target,
1988                condition: Expr::Var("control".into()),
1989            },
1990        ];
1991        let generators = [StabilizerGenerator::new(
1992            Stabilizer::from_interior_nodes([(branch_target, crate::Pauli::X)]),
1993            StabilizerRowKind::Measurement {
1994                name: "control".into(),
1995            },
1996        )];
1997
1998        let mut dag = ActionDag::from_actions(&actions);
1999        dag.branch_regions.push(BranchRegion::test_region(
2000            branch_target,
2001            vec![branch_target],
2002            Vec::new(),
2003        ));
2004        let err = dag
2005            .attach_readout_dependencies(&generators, None)
2006            .unwrap_err();
2007
2008        assert!(matches!(
2009            err,
2010            BlockGraphError::InvalidAction(InvalidActionError::DependencyCycle { .. })
2011        ));
2012    }
2013}