Skip to main content

bloq_graph/
guarded.rs

1//! Local topology alternatives and their exact Boolean applicability.
2//!
3//! A block signature reads its own block, incident pipes, and neighboring block
4//! kinds. Enumerate only that neighborhood, keeping distant branch choices as
5//! shared Boolean functions. Component-wide facts can refine these guards later.
6
7use std::collections::{BTreeMap, BTreeSet, HashMap};
8use std::sync::Arc;
9
10use bloq_utils::boolean::{
11    BooleanDecisionDiagram, BooleanOp, BooleanResourceError, DECISION_FALSE, DECISION_TRUE,
12    DecisionId,
13};
14use glam::IVec3;
15
16use crate::{
17    Action, Block, BlockGraph, BlockGraphError, BlockKind, BranchRegion, Expr,
18    ModuleCertificationLimits, Pipe, StabilizerError,
19};
20
21#[doc(hidden)]
22#[derive(Debug, Clone)]
23pub struct GuardedProjection {
24    pub guard: DecisionId,
25    /// Geometry context for this entry's center (the key in `GuardedTopology::sites`).
26    /// The center and its immediate neighbors have their exact normalized kinds
27    /// and complete selected pipe incidence. Outer halo blocks only resolve those
28    /// pipe endpoints; their kinds/incidence are not a complete physical program.
29    /// Unchanged contexts share one complete reference graph, which can carry
30    /// source actions; deviations contain geometry only. Consumers read only
31    /// this center's geometry. Never re-normalize a context or emit its halo.
32    pub graph: Arc<BlockGraph>,
33}
34
35#[doc(hidden)]
36#[derive(Debug, Clone)]
37pub enum GuardedVariable {
38    Branch { name: String, condition: DecisionId },
39    Outcome(String),
40}
41
42/// The source-selector name used to pin a selective measurement at `position`.
43///
44/// Coordinates are those of the normalized graph supplied to compilation.
45/// The punctuation keeps these generated names separate from authored branch names.
46#[must_use]
47pub fn selective_selector_name(position: IVec3) -> String {
48    format!("selective [{},{},{}]", position.x, position.y, position.z)
49}
50
51/// Availability for one fixed set of known outcomes. Decision nodes may be
52/// appended while querying, but the query must end before collection renumbers
53/// them or another outcome becomes known.
54pub(crate) struct AvailabilityQuery {
55    // None is an unknown outcome. A known outcome needs no further input;
56    // a structural variable instead requires its source condition.
57    requirements: Vec<Option<DecisionId>>,
58    values: crate::FxHashMap<DecisionId, bool>,
59    pending: Vec<DecisionId>,
60}
61
62impl AvailabilityQuery {
63    fn cached(&self, root: DecisionId) -> Option<bool> {
64        if root.0 < 2 {
65            Some(true)
66        } else {
67            self.values.get(&root).copied()
68        }
69    }
70
71    pub(crate) fn available(
72        &mut self,
73        root: DecisionId,
74        diagram: &mut BooleanDecisionDiagram,
75    ) -> Result<bool, BooleanResourceError> {
76        if let Some(value) = self.cached(root) {
77            return Ok(value);
78        }
79        self.pending.clear();
80        self.pending.push(root);
81        while let Some(&root) = self.pending.last() {
82            diagram.charge(1)?;
83            if self.cached(root).is_some() {
84                self.pending.pop();
85                continue;
86            }
87            let available = if let Some(node) = diagram.node(root) {
88                if let Some(condition) = self.requirements[node.variable] {
89                    let inputs = [condition, node.low, node.high];
90                    if inputs
91                        .iter()
92                        .any(|&input| self.cached(input) == Some(false))
93                    {
94                        false
95                    } else if let Some(input) = inputs
96                        .into_iter()
97                        .find(|&input| self.cached(input).is_none())
98                    {
99                        self.pending.push(input);
100                        continue;
101                    } else {
102                        true
103                    }
104                } else {
105                    false
106                }
107            } else {
108                true
109            };
110            self.values.insert(root, available);
111            self.pending.pop();
112        }
113        Ok(self
114            .cached(root)
115            .expect("the requested function was visited"))
116    }
117}
118
119/// Symbolic image of source Boolean functions. Disjoint source-variable
120/// supports factor, even when their output coordinates are interleaved.
121fn boolean_image(
122    source: &BooleanDecisionDiagram,
123    output: &mut BooleanDecisionDiagram,
124    roots: Vec<(usize, DecisionId)>,
125    limit: usize,
126) -> Result<DecisionId, BlockGraphError> {
127    // One traversal of the shared source DAG: a repeated node or source
128    // variable joins the roots that use it. Constant roots stay isolated.
129    output.charge(roots.len())?;
130    let mut components = petgraph::unionfind::UnionFind::new(roots.len());
131    let mut node_owner = HashMap::new();
132    let mut variable_owner = HashMap::new();
133    let mut pending = Vec::new();
134    for (root_index, &(_, root)) in roots.iter().enumerate() {
135        pending.push(root);
136        while let Some(id) = pending.pop() {
137            output.charge(1)?;
138            let Some(node) = source.node(id) else {
139                continue;
140            };
141            if let Some(&prior) = node_owner.get(&id) {
142                output.charge(1)?;
143                components.union(root_index, prior);
144                continue;
145            }
146            node_owner.insert(id, root_index);
147            if let Some(&prior) = variable_owner.get(&node.variable) {
148                output.charge(1)?;
149                components.union(root_index, prior);
150            } else {
151                variable_owner.insert(node.variable, root_index);
152            }
153            pending.extend([node.low, node.high]);
154        }
155    }
156
157    output.charge(roots.len())?;
158    let mut groups = Vec::<Vec<(usize, DecisionId)>>::new();
159    let mut group_by_component = HashMap::new();
160    for (root_index, root) in roots.into_iter().enumerate() {
161        let component = components.find(root_index);
162        let group = *group_by_component.entry(component).or_insert_with(|| {
163            groups.push(Vec::new());
164            groups.len() - 1
165        });
166        groups[group].push(root);
167    }
168
169    let mut visited = 0;
170    let mut image = DECISION_TRUE;
171    for group in groups {
172        output.charge(1)?;
173        let factor = match group.as_slice() {
174            [(index, root)] if *root == DECISION_TRUE => {
175                output.make_node(*index, DECISION_FALSE, DECISION_TRUE)?
176            }
177            [(index, root)] if *root == DECISION_FALSE => {
178                output.make_node(*index, DECISION_TRUE, DECISION_FALSE)?
179            }
180            [(_, _)] => {
181                // A nonconstant reduced BDD reaches both truth values. One
182                // output bit alone therefore has an unconstrained image.
183                visit_image_state(&mut visited, limit)?;
184                DECISION_TRUE
185            }
186            _ => boolean_image_component(source, output, group, &mut visited, limit)?,
187        };
188        image = output.apply(BooleanOp::And, image, factor)?;
189    }
190    Ok(image)
191}
192
193fn visit_image_state(visited: &mut usize, limit: usize) -> Result<(), BlockGraphError> {
194    let next = visited.checked_add(1);
195    *visited = next.unwrap_or(usize::MAX);
196    if next.is_none() || *visited > limit {
197        return Err(StabilizerError::ResourceLimited {
198            phase: "structural selector image",
199            observed: *visited,
200            limit,
201        }
202        .into());
203    }
204    Ok(())
205}
206
207/// Existing residual-tuple search, scoped to one correlated support component.
208/// `visited` is shared across all components, so partitioning cannot bypass
209/// the structural witness budget.
210fn boolean_image_component(
211    source: &BooleanDecisionDiagram,
212    output: &mut BooleanDecisionDiagram,
213    roots: Vec<(usize, DecisionId)>,
214    visited: &mut usize,
215    limit: usize,
216) -> Result<DecisionId, BlockGraphError> {
217    enum Step {
218        Visit(Vec<(usize, DecisionId)>),
219        Join {
220            remaining: Vec<(usize, DecisionId)>,
221            fixed: DecisionId,
222        },
223    }
224    let mut pending = vec![Step::Visit(roots)];
225    let mut values = Vec::new();
226    let mut memo = HashMap::new();
227    while let Some(step) = pending.pop() {
228        let roots = match step {
229            Step::Visit(roots) => roots,
230            Step::Join { remaining, fixed } => {
231                let high = values.pop().expect("visited high image");
232                let low = values.pop().expect("visited low image");
233                let image = output.apply(BooleanOp::Or, low, high)?;
234                memo.insert(remaining, image);
235                values.push(output.apply(BooleanOp::And, fixed, image)?);
236                continue;
237            }
238        };
239        output.charge(roots.len())?;
240        let mut fixed = DECISION_TRUE;
241        let mut remaining = Vec::new();
242        for (index, root) in roots {
243            if root.0 < 2 {
244                let value = if root == DECISION_TRUE {
245                    output.make_node(index, DECISION_FALSE, DECISION_TRUE)?
246                } else {
247                    output.make_node(index, DECISION_TRUE, DECISION_FALSE)?
248                };
249                fixed = output.apply(BooleanOp::And, fixed, value)?;
250            } else {
251                remaining.push((index, root));
252            }
253        }
254        if remaining.is_empty() {
255            values.push(fixed);
256            continue;
257        }
258        if let Some(&image) = memo.get(&remaining) {
259            values.push(output.apply(BooleanOp::And, fixed, image)?);
260            continue;
261        }
262        visit_image_state(visited, limit)?;
263        let variable = remaining
264            .iter()
265            .map(|(_, root)| {
266                source
267                    .node(*root)
268                    .expect("nonterminal root belongs to the source diagram")
269                    .variable
270            })
271            .min()
272            .expect("remaining source roots are nonempty");
273        output.charge(remaining.len().saturating_mul(2))?;
274        let branch = |high| {
275            remaining
276                .iter()
277                .map(|&(index, root)| {
278                    let node = source
279                        .node(root)
280                        .expect("nonterminal root belongs to the source diagram");
281                    (
282                        index,
283                        if node.variable != variable {
284                            root
285                        } else if high {
286                            node.high
287                        } else {
288                            node.low
289                        },
290                    )
291                })
292                .collect()
293        };
294        let low = branch(false);
295        let high = branch(true);
296        pending.extend([
297            Step::Join { remaining, fixed },
298            Step::Visit(high),
299            Step::Visit(low),
300        ]);
301    }
302    Ok(values.pop().expect("the source image produces one value"))
303}
304
305fn block_footprint_size<'a>(blocks: impl Iterator<Item = &'a crate::Block>) -> [usize; 2] {
306    blocks.fold([0usize; 2], |[count, cells], block| {
307        let footprint = if block.kind().is_cube() {
308            block.height_cells() as usize
309        } else {
310            block.kind().reserved_offsets().len()
311        };
312        [count.saturating_add(1), cells.saturating_add(footprint)]
313    })
314}
315
316type LocalChoices = HashMap<DecisionId, bool>;
317
318struct LocalGeometry {
319    blocks: Vec<Block>,
320    pipes: Vec<Pipe>,
321    primary_incidence: Vec<(IVec3, usize)>,
322}
323
324impl LocalGeometry {
325    fn matches_reference(&self, reference: &BlockGraph) -> bool {
326        self.primary_incidence.iter().all(|&(position, count)| {
327            let block = self
328                .blocks
329                .binary_search_by_key(&position.to_array(), |block| block.pos().to_array())
330                .map(|index| &self.blocks[index])
331                .expect("local primary block is retained");
332            reference.get_block(position) == Some(block)
333                && reference.pipes_at(position).count() == count
334        }) && self
335            .pipes
336            .iter()
337            .all(|pipe| reference.get_pipe(pipe.src(), pipe.dst()) == Some(pipe))
338    }
339}
340
341#[derive(Debug)]
342struct Authored<T> {
343    choice: Option<(DecisionId, bool)>,
344    value: T,
345}
346
347impl<T> Authored<T> {
348    fn selected(&self, choices: &LocalChoices) -> bool {
349        self.choice
350            .is_none_or(|(root, value)| choices.get(&root).copied().unwrap_or(false) == value)
351    }
352}
353
354/// One source-wide index. Local materialization only visits incident records;
355/// distant branch choices are irrelevant to the center and its neighbors.
356#[derive(Debug)]
357struct AuthoredGeometry {
358    blocks: Vec<Authored<Block>>,
359    pipes: Vec<Authored<Pipe>>,
360    blocks_at: BTreeMap<[i32; 3], Vec<usize>>,
361    endpoint_owners: crate::FxHashMap<IVec3, Vec<usize>>,
362    incidence: crate::FxHashMap<IVec3, Vec<usize>>,
363    owners: crate::FxHashMap<IVec3, BTreeSet<DecisionId>>,
364    neighbors: crate::FxHashMap<IVec3, BTreeSet<[i32; 3]>>,
365}
366
367impl AuthoredGeometry {
368    fn new(
369        source: &BlockGraph,
370        regions: &[BranchRegion],
371        shared_pipes: &[Pipe],
372        branch_roots: &HashMap<IVec3, DecisionId>,
373    ) -> Result<Self, BlockGraphError> {
374        let shown = regions
375            .iter()
376            .flat_map(|region| region.shown_arm().blocks().map(Block::pos))
377            .collect::<std::collections::HashSet<_>>();
378        let mut blocks = source
379            .blocks()
380            .filter(|block| !shown.contains(&block.pos()))
381            .cloned()
382            .map(|value| Authored {
383                choice: None,
384                value,
385            })
386            .collect::<Vec<_>>();
387        let mut pipes = Vec::new();
388        for region in regions {
389            for value in [false, true] {
390                let choice = Some((branch_roots[&region.target], value));
391                blocks.extend(
392                    region
393                        .arm(value)
394                        .blocks()
395                        .cloned()
396                        .map(|value| Authored { choice, value }),
397                );
398                pipes.extend(
399                    region
400                        .arm(value)
401                        .pipes()
402                        .cloned()
403                        .map(|value| Authored { choice, value }),
404                );
405            }
406        }
407        // A selected arm pipe takes precedence over an identical common seam,
408        // matching materialize_projection's add-arms-then-shared-pipes order.
409        pipes.extend(shared_pipes.iter().cloned().map(|value| Authored {
410            choice: None,
411            value,
412        }));
413        let mut index = Self {
414            blocks,
415            pipes,
416            blocks_at: BTreeMap::new(),
417            endpoint_owners: crate::FxHashMap::default(),
418            incidence: crate::FxHashMap::default(),
419            owners: crate::FxHashMap::default(),
420            neighbors: crate::FxHashMap::default(),
421        };
422        for (id, record) in index.blocks.iter().enumerate() {
423            let position = record.value.pos();
424            index
425                .blocks_at
426                .entry(position.to_array())
427                .or_default()
428                .push(id);
429            if let Some((root, _)) = record.choice {
430                index.owners.entry(position).or_default().insert(root);
431            }
432            for offset in record.value.connectable_offsets() {
433                let endpoint = crate::checked_add_position(position, offset)?;
434                index.endpoint_owners.entry(endpoint).or_default().push(id);
435            }
436        }
437        for (id, record) in index.pipes.iter().enumerate() {
438            let (left, right) = record.value.try_endpoints()?;
439            let left = index
440                .endpoint_owners
441                .get(&left)
442                .into_iter()
443                .flatten()
444                .map(|&block| index.blocks[block].value.pos())
445                .collect::<std::collections::HashSet<_>>();
446            let right = index
447                .endpoint_owners
448                .get(&right)
449                .into_iter()
450                .flatten()
451                .map(|&block| index.blocks[block].value.pos())
452                .collect::<std::collections::HashSet<_>>();
453            for &position in left.iter().chain(&right) {
454                index.incidence.entry(position).or_default().push(id);
455                if let Some((root, _)) = record.choice {
456                    index.owners.entry(position).or_default().insert(root);
457                }
458            }
459            for &left in &left {
460                for &right in &right {
461                    index
462                        .neighbors
463                        .entry(left)
464                        .or_default()
465                        .insert(right.to_array());
466                    index
467                        .neighbors
468                        .entry(right)
469                        .or_default()
470                        .insert(left.to_array());
471                }
472            }
473        }
474        for records in index.incidence.values_mut() {
475            records.sort_unstable();
476            records.dedup();
477        }
478        Ok(index)
479    }
480
481    fn block_at(
482        &self,
483        position: IVec3,
484        choices: &LocalChoices,
485    ) -> Result<Option<&Block>, BlockGraphError> {
486        let mut selected = self
487            .blocks_at
488            .get(&position.to_array())
489            .into_iter()
490            .flatten()
491            .map(|&id| &self.blocks[id])
492            .filter(|record| record.selected(choices));
493        let block = selected.next().map(|record| &record.value);
494        if selected.next().is_some() {
495            return Err(BlockGraphError::BlockExists(position));
496        }
497        Ok(block)
498    }
499
500    fn endpoint_owner(
501        &self,
502        endpoint: IVec3,
503        choices: &LocalChoices,
504    ) -> Result<&Block, BlockGraphError> {
505        self.endpoint_owners
506            .get(&endpoint)
507            .into_iter()
508            .flatten()
509            .map(|&id| &self.blocks[id])
510            .find(|record| record.selected(choices))
511            .map(|record| &record.value)
512            .ok_or(BlockGraphError::BlockNotFound(endpoint))
513    }
514
515    fn pipes_at(
516        &self,
517        position: IVec3,
518        choices: &LocalChoices,
519    ) -> Result<Vec<&Pipe>, BlockGraphError> {
520        let mut selected = BTreeMap::<([i32; 3], [i32; 3]), usize>::new();
521        for &id in self.incidence.get(&position).into_iter().flatten() {
522            let record = &self.pipes[id];
523            if !record.selected(choices) {
524                continue;
525            }
526            let (left, right) = record.value.endpoints();
527            if self.endpoint_owner(left, choices)?.pos() != position
528                && self.endpoint_owner(right, choices)?.pos() != position
529            {
530                continue;
531            }
532            let (left, right) = crate::branch::pipe_key(&record.value);
533            let key = (left.to_array(), right.to_array());
534            if let Some(&previous) = selected.get(&key) {
535                if record.choice.is_some() && self.pipes[previous].choice.is_some() {
536                    return Err(BlockGraphError::PipeExists(left, right));
537                }
538            } else {
539                selected.insert(key, id);
540            }
541        }
542        Ok(selected
543            .into_values()
544            .map(|id| &self.pipes[id].value)
545            .collect())
546    }
547
548    fn roots(&self, position: IVec3) -> Vec<DecisionId> {
549        std::iter::once(position.to_array())
550            .chain(self.neighbors.get(&position).into_iter().flatten().copied())
551            .flat_map(|position| {
552                self.owners
553                    .get(&IVec3::from_array(position))
554                    .into_iter()
555                    .flatten()
556                    .copied()
557            })
558            .collect::<BTreeSet<_>>()
559            .into_iter()
560            .collect()
561    }
562
563    fn local(
564        &self,
565        center: IVec3,
566        choices: &LocalChoices,
567    ) -> Result<Option<LocalGeometry>, BlockGraphError> {
568        if self.block_at(center, choices)?.is_none() {
569            return Ok(None);
570        }
571        let mut primary = BTreeSet::from([center.to_array()]);
572        for pipe in self.pipes_at(center, choices)? {
573            for endpoint in [pipe.src(), pipe.dst()] {
574                primary.insert(self.endpoint_owner(endpoint, choices)?.pos().to_array());
575            }
576        }
577        let mut blocks = BTreeMap::new();
578        let mut pipes = BTreeMap::new();
579        let mut primary_incidence = Vec::with_capacity(primary.len());
580        for &position in &primary {
581            let position = IVec3::from_array(position);
582            let mut block = self
583                .block_at(position, choices)?
584                .expect("selected neighbor exists")
585                .clone();
586            let mut counts = [0usize; 3];
587            for pipe in self.pipes_at(position, choices)? {
588                counts[pipe.dir().as_udirection().index()] += 1;
589                for endpoint in [pipe.src(), pipe.dst()] {
590                    // Unmentioned controls only choose lookup-only halo blocks;
591                    // primary kinds/pipes depend entirely on the local case roots.
592                    let neighbor = self.endpoint_owner(endpoint, choices)?;
593                    blocks
594                        .entry(neighbor.pos().to_array())
595                        .or_insert_with(|| neighbor.clone());
596                }
597                let (left, right) = crate::branch::pipe_key(pipe);
598                pipes.insert((left.to_array(), right.to_array()), pipe.clone());
599            }
600            if let BlockKind::Cube(kind) = block.kind() {
601                block.kind = BlockKind::Cube(crate::graph::shadowed_cube_kind(kind, counts));
602            }
603            primary_incidence.push((position, counts.iter().sum()));
604            blocks.insert(position.to_array(), block);
605        }
606        Ok(Some(LocalGeometry {
607            blocks: blocks.into_values().collect(),
608            pipes: pipes.into_values().collect(),
609            primary_incidence,
610        }))
611    }
612}
613
614/// A shared decision diagram over source measurement variables, with local
615/// structural alternatives. It never lists the full joint branch domain.
616#[doc(hidden)]
617#[derive(Debug)]
618pub struct GuardedTopology {
619    pub source: BlockGraph,
620    pub diagram: BooleanDecisionDiagram,
621    pub variables: Vec<GuardedVariable>,
622    /// Reachable branch-vector image of source outcome assignments. This does
623    /// not tie each Branch variable to its condition for a particular Outcome
624    /// assignment; [`Self::evaluate_source`] supplies those branch values.
625    pub domain: DecisionId,
626    pub branches: Vec<(String, IVec3, DecisionId)>,
627    pub resolves: Vec<(IVec3, DecisionId)>,
628    pub sites: BTreeMap<[i32; 3], Vec<GuardedProjection>>,
629    projections: HashMap<Vec<bool>, Arc<BlockGraph>>,
630    regions: Vec<BranchRegion>,
631    shared_pipes: Vec<Pipe>,
632    conditions: Vec<(Arc<Expr>, DecisionId)>,
633    condition_slots: HashMap<Arc<Expr>, usize>,
634    named_conditions: HashMap<String, usize>,
635    limits: ModuleCertificationLimits,
636    common_projection_size: [usize; 2],
637    retained_projection_size: [usize; 2],
638    deferred_sites: bool,
639}
640
641impl GuardedTopology {
642    pub(crate) fn decision_root_count(&self) -> usize {
643        1 + self.conditions.len()
644            + self.branches.len()
645            + self.resolves.len()
646            + self.sites.values().map(Vec::len).sum::<usize>()
647            + self
648                .variables
649                .iter()
650                .filter(|variable| matches!(variable, GuardedVariable::Branch { .. }))
651                .count()
652    }
653
654    /// Collect only while no external stage retains unlisted decision ids.
655    pub(crate) fn collect_garbage<'a>(
656        &'a mut self,
657        additional: impl IntoIterator<Item = &'a mut DecisionId>,
658    ) -> usize {
659        assert!(
660            !self.deferred_sites,
661            "deferred authored-geometry roots must be consumed before Boolean collection"
662        );
663        let roots = std::iter::once(&mut self.domain)
664            .chain(self.conditions.iter_mut().map(|(_, root)| root))
665            .chain(self.branches.iter_mut().map(|(_, _, root)| root))
666            .chain(self.resolves.iter_mut().map(|(_, root)| root))
667            .chain(
668                self.sites
669                    .values_mut()
670                    .flatten()
671                    .map(|site| &mut site.guard),
672            )
673            .chain(
674                self.variables
675                    .iter_mut()
676                    .filter_map(|variable| match variable {
677                        GuardedVariable::Branch { condition, .. } => Some(condition),
678                        GuardedVariable::Outcome(_) => None,
679                    }),
680            );
681        self.diagram.collect_garbage(roots.chain(additional))
682    }
683
684    /// Builds the factored guarded topology for a source graph.
685    ///
686    /// # Errors
687    ///
688    /// Returns an error if source validation, projection, or resource accounting fails.
689    ///
690    /// # Panics
691    ///
692    /// Panics if validated branch actions do not name their stored regions.
693    pub fn new(
694        source: &BlockGraph,
695        limits: ModuleCertificationLimits,
696    ) -> Result<Self, BlockGraphError> {
697        let (mut topology, sites) = Self::new_with_geometry(source, limits)?;
698        topology.populate_sites(sites)?;
699        Ok(topology)
700    }
701
702    fn new_with_geometry(
703        source: &BlockGraph,
704        limits: ModuleCertificationLimits,
705    ) -> Result<(Self, AuthoredGeometry), BlockGraphError> {
706        source.require_flat_hierarchy("guarded topology")?;
707        source.validate_resource_limits(limits)?;
708        crate::validate::validate_source(source)?;
709        let actions = source.actions();
710        // validate_source already checked every arm and its action binding.
711        // Recover that resolve order without rebuilding candidate arm graphs.
712        let by_target = source
713            .branch_definitions()
714            .iter()
715            .map(|region| (region.target, region))
716            .collect::<HashMap<_, _>>();
717        let regions = actions
718            .iter()
719            .filter_map(|action| match action {
720                Action::Branch { target, .. } => Some((*by_target[target]).clone()),
721                _ => None,
722            })
723            .collect::<Vec<_>>();
724        let shown = regions
725            .iter()
726            .flat_map(|region| {
727                region
728                    .shown_arm()
729                    .blocks()
730                    .map(|block| block.pos().to_array())
731            })
732            .collect::<BTreeSet<_>>();
733        let common_projection_size = block_footprint_size(
734            source
735                .blocks()
736                .filter(|block| !shown.contains(&block.pos().to_array())),
737        );
738        let expressions = actions
739            .iter()
740            .filter_map(|action| match action {
741                Action::Branch { condition, .. }
742                | Action::Resolve { condition, .. }
743                | Action::Feedback {
744                    condition: Some(condition),
745                    ..
746                }
747                | Action::DiscardIf(condition) => Some(condition.clone()),
748                Action::Let { expr, .. } => Some(expr.clone()),
749                Action::Measure { name, .. } => Some(Expr::Var(name.clone())),
750                _ => None,
751            })
752            .map(Arc::new)
753            .collect::<Vec<_>>();
754        // Slots stay fixed when roots are remapped. Keep the first exact
755        // occurrence, matching the ordered condition search.
756        let mut condition_slots = HashMap::new();
757        for (slot, expression) in expressions.iter().enumerate() {
758            condition_slots
759                .entry(Arc::clone(expression))
760                .or_insert(slot);
761        }
762        let aliases = actions
763            .iter()
764            .filter_map(|action| match action {
765                Action::Let { name, expr } => Some((name.as_str(), expr)),
766                _ => None,
767            })
768            .collect();
769        let decisions = crate::action_dag::ResolveDecisionDiagram::build(
770            &expressions.iter().map(Arc::as_ref).collect::<Vec<_>>(),
771            &aliases,
772            limits.boolean_limits(),
773        )?
774        .expect("validated source expressions");
775        let mut diagram = BooleanDecisionDiagram::with_limits(limits.boolean_limits());
776        diagram.charge(decisions.diagram.steps())?;
777        let mut outcomes = vec![String::new(); decisions.variable_indices.len()];
778        for (name, index) in decisions.variable_indices {
779            outcomes[index] = name;
780        }
781        let named_source_conditions = expressions
782            .iter()
783            .zip(&decisions.roots)
784            .filter_map(|(expression, root)| match expression.as_ref() {
785                Expr::Var(name) => Some((name.as_str(), *root)),
786                _ => None,
787            })
788            .collect::<HashMap<_, _>>();
789        let source_condition = |expr: &Expr| {
790            if let Expr::Var(name) = expr {
791                return named_source_conditions[name.as_str()];
792            }
793            decisions.roots[*condition_slots
794                .get(expr)
795                .expect("source conditions were indexed before lowering")]
796        };
797        let branch_sources = actions
798            .iter()
799            .filter_map(|action| match action {
800                Action::Branch { target, condition } => Some((
801                    by_target[target].name.clone(),
802                    *target,
803                    source_condition(condition),
804                )),
805                _ => None,
806            })
807            .collect::<Vec<_>>();
808        // Grouping every branch before every outcome makes a sum of local
809        // branch/outcome products exponential in an ordered decision diagram.
810        // Preserve source outcome order, placing each branch next to its last
811        // condition dependency. Source-node remapping then stays monotone.
812        diagram.charge(
813            decisions
814                .diagram
815                .nodes()
816                .len()
817                .saturating_add(outcomes.len())
818                .saturating_add(branch_sources.len()),
819        )?;
820        let mut last_dependency = vec![None, None];
821        for node in decisions.diagram.nodes() {
822            let last = std::iter::once(node.variable)
823                .chain(last_dependency[node.low.0])
824                .chain(last_dependency[node.high.0])
825                .max();
826            last_dependency.push(last);
827        }
828        let mut after = vec![Vec::new(); outcomes.len() + 1];
829        for (branch, (_, _, condition)) in branch_sources.iter().enumerate() {
830            after[last_dependency[condition.0].map_or(0, |index| index + 1)].push(branch);
831        }
832        let mut outcome_indices = vec![0; outcomes.len()];
833        let mut branch_indices = vec![0; branch_sources.len()];
834        let mut variables = Vec::with_capacity(outcomes.len() + branch_sources.len());
835        for (slot, outcome) in outcomes.into_iter().map(Some).chain([None]).enumerate() {
836            for &branch in &after[slot] {
837                branch_indices[branch] = variables.len();
838                let (name, _, condition) = &branch_sources[branch];
839                variables.push(GuardedVariable::Branch {
840                    name: name.clone(),
841                    condition: *condition,
842                });
843            }
844            if let Some(name) = outcome {
845                outcome_indices[slot] = variables.len();
846                variables.push(GuardedVariable::Outcome(name));
847            }
848        }
849        let mut remap = vec![DECISION_FALSE, DECISION_TRUE];
850        for node in decisions.diagram.nodes() {
851            remap.push(diagram.make_node(
852                outcome_indices[node.variable],
853                remap[node.low.0],
854                remap[node.high.0],
855            )?);
856        }
857        for variable in &mut variables {
858            if let GuardedVariable::Branch { condition, .. } = variable {
859                *condition = remap[condition.0];
860            }
861        }
862        let conditions = expressions
863            .iter()
864            .cloned()
865            .zip(decisions.roots.iter().map(|root| remap[root.0]))
866            .collect::<Vec<_>>();
867        let named_conditions = conditions
868            .iter()
869            .enumerate()
870            .filter_map(|(index, (expression, _))| match expression.as_ref() {
871                Expr::Var(name) => Some((name.clone(), index)),
872                _ => None,
873            })
874            .collect();
875        let mut branch_functions = Vec::new();
876        let mut branches = Vec::new();
877        for ((name, target, condition), index) in branch_sources.into_iter().zip(branch_indices) {
878            let variable = diagram.make_node(index, DECISION_FALSE, DECISION_TRUE)?;
879            branch_functions.push((index, condition));
880            branches.push((name, target, variable));
881        }
882        let mut resolves = Vec::new();
883        for action in &actions {
884            if let Action::Resolve { target, condition } = action {
885                resolves.push((*target, remap[source_condition(condition).0]));
886            }
887        }
888        let domain = boolean_image(
889            &decisions.diagram,
890            &mut diagram,
891            branch_functions,
892            limits.max_witness_nodes,
893        )?;
894        drop(decisions.diagram);
895        let branch_root = branches
896            .iter()
897            .map(|(_, target, root)| (*target, *root))
898            .collect::<HashMap<_, _>>();
899        let shared_pipes = source.branch_shared_pipes();
900        let geometry = AuthoredGeometry::new(source, &regions, &shared_pipes, &branch_root)?;
901        let mut topology = Self {
902            source: source.clone(),
903            diagram,
904            variables,
905            domain,
906            branches,
907            resolves,
908            sites: BTreeMap::new(),
909            projections: HashMap::new(),
910            regions,
911            shared_pipes,
912            conditions,
913            condition_slots,
914            named_conditions,
915            limits,
916            common_projection_size,
917            retained_projection_size: [0; 2],
918            deferred_sites: true,
919        };
920        // The reference retains authored action order and output roles.
921        topology.project(DECISION_TRUE)?;
922        Ok((topology, geometry))
923    }
924
925    /// Materialize the guarded site views from authored local incidence.
926    ///
927    /// # Errors
928    /// Returns an error if local geometry or its configured budget fails.
929    fn populate_sites(&mut self, geometry: AuthoredGeometry) -> Result<(), BlockGraphError> {
930        // Global planning needs this one reference anyway. Unchanged local
931        // contexts borrow it instead of retaining another copy of every halo.
932        let reference = self.project(DECISION_TRUE)?;
933        for &position in geometry.blocks_at.keys() {
934            let pos = IVec3::from_array(position);
935            let variants = self.site_variants(&geometry, pos, &reference)?;
936            self.sites.insert(position, variants);
937        }
938        self.deferred_sites = false;
939        Ok(())
940    }
941
942    fn site_variants(
943        &mut self,
944        geometry: &AuthoredGeometry,
945        position: IVec3,
946        reference: &Arc<BlockGraph>,
947    ) -> Result<Vec<GuardedProjection>, BlockGraphError> {
948        let roots = geometry.roots(position);
949        let cases = self.cases(&roots, DECISION_TRUE)?;
950        let mut variants = Vec::new();
951        for (values, guard) in cases {
952            let choices = roots.iter().copied().zip(values).collect::<LocalChoices>();
953            let Some(local) = geometry.local(position, &choices)? else {
954                continue;
955            };
956            let graph = if local.matches_reference(reference) {
957                Arc::clone(reference)
958            } else {
959                self.reserve_geometry(block_footprint_size(local.blocks.iter()))?;
960                let mut graph = BlockGraph::new();
961                for block in local.blocks {
962                    graph.try_add_block(block)?;
963                }
964                for pipe in local.pipes {
965                    graph.try_add_pipe(pipe)?;
966                }
967                Arc::new(graph)
968            };
969            variants.push(GuardedProjection { guard, graph });
970        }
971        Ok(variants)
972    }
973
974    /// Returns the decision root for a source expression.
975    ///
976    /// # Panics
977    ///
978    /// Panics if `expr` was not part of the source used to build this topology.
979    pub fn condition(&self, expr: &Expr) -> DecisionId {
980        if let Expr::Var(name) = expr {
981            return self.conditions[self.named_conditions[name]].1;
982        }
983        self.conditions[*self.condition_slots.get(expr).expect("source expression")].1
984    }
985
986    /// Distinct values of these local functions, retaining each exact guard.
987    ///
988    /// # Errors
989    ///
990    /// Returns an error if Boolean work or the guarded domain exceeds its limit.
991    pub fn cases(
992        &mut self,
993        roots: &[DecisionId],
994        enabled: DecisionId,
995    ) -> Result<Vec<(Vec<bool>, DecisionId)>, BlockGraphError> {
996        let limit = self.limits.max_guarded_domain_size;
997        self.diagram.charge(roots.len().saturating_add(1))?;
998        let enabled = self.diagram.apply(BooleanOp::And, enabled, self.domain)?;
999        if enabled == DECISION_FALSE {
1000            return Ok(Vec::new());
1001        }
1002        // Refine the exact guards directly. Enumerating a joint decision
1003        // image first revisits residual tuples, then rebuilds these same guards.
1004        let mut cases = vec![(Vec::with_capacity(roots.len()), enabled)];
1005        for &root in roots {
1006            let mut next = Vec::new();
1007            for (mut values, guard) in cases {
1008                self.diagram.charge(1)?;
1009                let high = self.diagram.apply(BooleanOp::And, guard, root)?;
1010                let low = self.diagram.apply(BooleanOp::Xor, guard, high)?;
1011                let count =
1012                    usize::from(low != DECISION_FALSE) + usize::from(high != DECISION_FALSE);
1013                if next.len().saturating_add(count) > limit {
1014                    return Err(StabilizerError::ResourceLimited {
1015                        phase: "local structural patterns",
1016                        observed: next.len().saturating_add(count),
1017                        limit,
1018                    }
1019                    .into());
1020                }
1021                if high == DECISION_FALSE {
1022                    values.push(false);
1023                    next.push((values, low));
1024                } else if low == DECISION_FALSE {
1025                    values.push(true);
1026                    next.push((values, high));
1027                } else {
1028                    self.diagram.charge(values.len().saturating_add(1))?;
1029                    let mut lower = values.clone();
1030                    lower.push(false);
1031                    next.push((lower, low));
1032                    values.push(true);
1033                    next.push((values, high));
1034                }
1035            }
1036            cases = next;
1037        }
1038        for (_, guard) in &mut cases {
1039            self.diagram.charge(1)?;
1040            *guard = self.diagram.constrain(*guard, self.domain)?;
1041        }
1042        if cases.len() > limit {
1043            return Err(StabilizerError::ResourceLimited {
1044                phase: "local structural patterns",
1045                observed: cases.len(),
1046                limit,
1047            }
1048            .into());
1049        }
1050        Ok(cases)
1051    }
1052
1053    /// Returns one source-outcome witness satisfying `guard`.
1054    ///
1055    /// # Errors
1056    ///
1057    /// Returns an error if Boolean witness search exceeds its budget.
1058    pub fn witness(
1059        &mut self,
1060        guard: DecisionId,
1061    ) -> Result<Option<BTreeMap<usize, bool>>, BooleanResourceError> {
1062        self.diagram.conjunction_witness(&[self.domain, guard])
1063    }
1064
1065    pub fn evaluate_source(&self, root: DecisionId, outcome: impl Fn(&str) -> bool) -> bool {
1066        self.diagram
1067            .evaluate(root, |index| match &self.variables[index] {
1068                GuardedVariable::Outcome(name) => outcome(name),
1069                GuardedVariable::Branch { condition, .. } => {
1070                    self.diagram.evaluate(*condition, |index| {
1071                        let GuardedVariable::Outcome(name) = &self.variables[index] else {
1072                            unreachable!("branch conditions contain only outcome variables")
1073                        };
1074                        outcome(name)
1075                    })
1076                }
1077            })
1078    }
1079
1080    pub(crate) fn availability(
1081        &mut self,
1082        known: &BTreeSet<String>,
1083    ) -> Result<AvailabilityQuery, BooleanResourceError> {
1084        self.diagram.charge(self.variables.len())?;
1085        Ok(AvailabilityQuery {
1086            requirements: self
1087                .variables
1088                .iter()
1089                .map(|variable| match variable {
1090                    GuardedVariable::Outcome(name) => known.contains(name).then_some(DECISION_TRUE),
1091                    GuardedVariable::Branch { condition, .. } => Some(*condition),
1092                })
1093                .collect(),
1094            values: Default::default(),
1095            pending: Vec::new(),
1096        })
1097    }
1098
1099    /// Returns one complete branch assignment satisfying `guard`.
1100    ///
1101    /// # Errors
1102    ///
1103    /// Returns an error if witness search exceeds its Boolean budget.
1104    pub fn assignment(
1105        &mut self,
1106        guard: DecisionId,
1107    ) -> Result<Option<Vec<(IVec3, bool)>>, BooleanResourceError> {
1108        let Some(variables) = self.witness(guard)? else {
1109            return Ok(None);
1110        };
1111        Ok(Some(
1112            self.branches
1113                .iter()
1114                .map(|(_, target, root)| {
1115                    (
1116                        *target,
1117                        self.diagram.evaluate(*root, |index| {
1118                            variables.get(&index).copied().unwrap_or(false)
1119                        }),
1120                    )
1121                })
1122                .collect(),
1123        ))
1124    }
1125
1126    fn reserve_geometry(&mut self, size: [usize; 2]) -> Result<(), BlockGraphError> {
1127        let mut retained = self.retained_projection_size;
1128        for (index, (phase, limit)) in [
1129            (
1130                "retained projection blocks",
1131                self.limits.max_expanded_blocks,
1132            ),
1133            (
1134                "retained projection footprint cells",
1135                self.limits.max_occupied_cells,
1136            ),
1137        ]
1138        .into_iter()
1139        .enumerate()
1140        {
1141            let next = retained[index].checked_add(size[index]);
1142            if next.is_none_or(|next| next > limit) {
1143                return Err(StabilizerError::ResourceLimited {
1144                    phase,
1145                    observed: next.unwrap_or(usize::MAX),
1146                    limit,
1147                }
1148                .into());
1149            }
1150            retained[index] = next.expect("checked retained projection size");
1151        }
1152        self.retained_projection_size = retained;
1153        Ok(())
1154    }
1155
1156    /// Materializes and caches the branch projection selected by `guard`.
1157    ///
1158    /// # Errors
1159    ///
1160    /// Returns an error if witness search, resource accounting, or projection fails.
1161    ///
1162    /// # Panics
1163    ///
1164    /// Panics if `guard` does not select any reachable local case.
1165    pub fn project(&mut self, guard: DecisionId) -> Result<Arc<BlockGraph>, BlockGraphError> {
1166        let assignment = self.assignment(guard)?.expect("a local case is reachable");
1167        let key = assignment
1168            .iter()
1169            .map(|(_, value)| *value)
1170            .collect::<Vec<_>>();
1171        if let Some(graph) = self.projections.get(&key) {
1172            return Ok(Arc::clone(graph));
1173        }
1174        let mut size = self.common_projection_size;
1175        for (region, &value) in self.regions.iter().zip(&key) {
1176            for (count, amount) in size
1177                .iter_mut()
1178                .zip(block_footprint_size(region.arm(value).blocks()))
1179            {
1180                *count = count
1181                    .checked_add(amount)
1182                    .expect("a projection is bounded by validated source geometry");
1183            }
1184        }
1185        self.reserve_geometry(size)?;
1186        let graph = Arc::new(self.source.project_validated_branches_deferred(
1187            &self.regions,
1188            &self.shared_pipes,
1189            &key,
1190        )?);
1191        self.projections.insert(key, Arc::clone(&graph));
1192        Ok(graph)
1193    }
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198    #[test]
1199    fn selector_image_counter_overflow_is_a_resource_failure() {
1200        let mut visited = usize::MAX - 1;
1201        visit_image_state(&mut visited, usize::MAX).expect("last representable state is allowed");
1202        assert!(matches!(
1203            visit_image_state(&mut visited, usize::MAX),
1204            Err(BlockGraphError::Stabilizer(
1205                StabilizerError::ResourceLimited {
1206                    phase: "structural selector image",
1207                    observed: usize::MAX,
1208                    limit: usize::MAX,
1209                }
1210            ))
1211        ));
1212        assert_eq!(visited, usize::MAX);
1213    }
1214
1215    use super::*;
1216
1217    #[test]
1218    fn availability_queries_preserve_dependencies_across_waves_and_collection() {
1219        fn reference(
1220            topology: &GuardedTopology,
1221            root: DecisionId,
1222            known: &BTreeSet<String>,
1223        ) -> bool {
1224            topology
1225                .diagram
1226                .variables(root)
1227                .iter()
1228                .all(|&index| match &topology.variables[index] {
1229                    GuardedVariable::Outcome(name) => known.contains(name),
1230                    GuardedVariable::Branch { condition, .. } => {
1231                        reference(topology, *condition, known)
1232                    }
1233                })
1234        }
1235
1236        let mut topology =
1237            GuardedTopology::new(&BlockGraph::new(), ModuleCertificationLimits::DEFAULT).unwrap();
1238        let outcomes = (0..3)
1239            .map(|index| {
1240                topology
1241                    .diagram
1242                    .make_node(index, DECISION_FALSE, DECISION_TRUE)
1243                    .unwrap()
1244            })
1245            .collect::<Vec<_>>();
1246        topology.variables = ["a", "b", "c"]
1247            .map(|name| GuardedVariable::Outcome(name.into()))
1248            .into();
1249        let condition = topology
1250            .diagram
1251            .apply(BooleanOp::Xor, outcomes[0], outcomes[1])
1252            .unwrap();
1253        topology.variables.push(GuardedVariable::Branch {
1254            name: "choice".into(),
1255            condition,
1256        });
1257        let branch = topology
1258            .diagram
1259            .make_node(3, DECISION_FALSE, DECISION_TRUE)
1260            .unwrap();
1261        let mut roots = vec![DECISION_FALSE, DECISION_TRUE, condition, branch];
1262        roots.extend(outcomes);
1263        for index in 0..48 {
1264            let op = [BooleanOp::And, BooleanOp::Or, BooleanOp::Xor][index % 3];
1265            let root = topology
1266                .diagram
1267                .apply(
1268                    op,
1269                    roots[index % roots.len()],
1270                    roots[(index * 7 + 3) % roots.len()],
1271                )
1272                .unwrap();
1273            roots.push(root);
1274        }
1275        for mask in 0..8 {
1276            let known = ["a", "b", "c"]
1277                .into_iter()
1278                .enumerate()
1279                .filter(|&(index, _)| mask & (1 << index) != 0)
1280                .map(|(_, name)| name.to_owned())
1281                .collect();
1282            let mut query = topology.availability(&known).unwrap();
1283            for &root in &roots {
1284                let expected = reference(&topology, root, &known);
1285                assert_eq!(
1286                    query.available(root, &mut topology.diagram).unwrap(),
1287                    expected
1288                );
1289            }
1290            let spent = topology.diagram.steps();
1291            for &root in roots.iter().rev() {
1292                query.available(root, &mut topology.diagram).unwrap();
1293            }
1294            assert_eq!(
1295                topology.diagram.steps(),
1296                spent,
1297                "shared roots need no second traversal"
1298            );
1299            let appended = topology
1300                .diagram
1301                .apply(BooleanOp::Xor, roots[mask + 2], roots[mask + 7])
1302                .unwrap();
1303            let expected = reference(&topology, appended, &known);
1304            assert_eq!(
1305                query.available(appended, &mut topology.diagram).unwrap(),
1306                expected
1307            );
1308            roots.push(appended);
1309            drop(query);
1310            topology.collect_garbage(roots.iter_mut());
1311        }
1312    }
1313
1314    #[test]
1315    fn uncached_availability_checks_spend_the_boolean_work_budget() {
1316        let mut topology =
1317            GuardedTopology::new(&BlockGraph::new(), ModuleCertificationLimits::DEFAULT).unwrap();
1318        topology.diagram =
1319            BooleanDecisionDiagram::with_limits(bloq_utils::boolean::BooleanLimits {
1320                max_nodes: 4,
1321                max_steps: 16,
1322            });
1323        topology.variables = vec![GuardedVariable::Outcome("a".into())];
1324        let root = topology
1325            .diagram
1326            .make_node(0, DECISION_FALSE, DECISION_TRUE)
1327            .unwrap();
1328        let mut query = topology
1329            .availability(&BTreeSet::from(["a".into()]))
1330            .unwrap();
1331        let remaining = topology.diagram.limits().max_steps - topology.diagram.steps();
1332        topology.diagram.charge(remaining).unwrap();
1333        assert!(matches!(
1334            query.available(root, &mut topology.diagram),
1335            Err(BooleanResourceError {
1336                resource: "Boolean work steps",
1337                observed: 17,
1338                limit: 16,
1339            })
1340        ));
1341    }
1342
1343    #[test]
1344    fn selector_image_preserves_correlations_with_interleaved_variable_indices() {
1345        let mut source = BooleanDecisionDiagram::default();
1346        let a = source.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
1347        let b = source.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
1348        let parity = source.apply(BooleanOp::Xor, a, b).unwrap();
1349        let mut output = BooleanDecisionDiagram::default();
1350        let image = boolean_image(
1351            &source,
1352            &mut output,
1353            vec![(6, a), (1, parity), (4, b), (8, DECISION_TRUE)],
1354            100,
1355        )
1356        .unwrap();
1357        for values in 0..16 {
1358            let selected = |slot: usize| values & (1 << slot) != 0;
1359            let actual = output.evaluate(image, |index| match index {
1360                6 => selected(0),
1361                1 => selected(1),
1362                4 => selected(2),
1363                8 => selected(3),
1364                _ => false,
1365            });
1366            assert_eq!(
1367                actual,
1368                selected(3) && selected(1) == (selected(0) ^ selected(2))
1369            );
1370        }
1371    }
1372
1373    #[test]
1374    fn selector_image_factors_two_interleaved_correlations_and_a_constant() {
1375        let mut source = BooleanDecisionDiagram::default();
1376        let a = source.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
1377        let b = source.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
1378        let not_a = source.negate(a).unwrap();
1379        let not_b = source.negate(b).unwrap();
1380        let mut output = BooleanDecisionDiagram::default();
1381        let image = boolean_image(
1382            &source,
1383            &mut output,
1384            vec![
1385                (6, a),
1386                (1, not_a),
1387                (4, b),
1388                (8, not_b),
1389                (3, DECISION_TRUE),
1390                (5, DECISION_FALSE),
1391            ],
1392            2,
1393        )
1394        .unwrap();
1395        for values in 0..64 {
1396            let selected = |slot: usize| values & (1 << slot) != 0;
1397            let actual = output.evaluate(image, |index| match index {
1398                6 => selected(0),
1399                1 => selected(1),
1400                4 => selected(2),
1401                8 => selected(3),
1402                3 => selected(4),
1403                5 => selected(5),
1404                _ => false,
1405            });
1406            assert_eq!(
1407                actual,
1408                selected(4)
1409                    && !selected(5)
1410                    && selected(0) != selected(1)
1411                    && selected(2) != selected(3)
1412            );
1413        }
1414    }
1415
1416    #[test]
1417    fn selector_image_shares_witness_and_work_limits_across_components() {
1418        let mut source = BooleanDecisionDiagram::default();
1419        let a = source.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
1420        let b = source.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
1421        let not_a = source.negate(a).unwrap();
1422        let not_b = source.negate(b).unwrap();
1423        for pair in [[(6, a), (1, not_a)], [(4, b), (8, not_b)]] {
1424            boolean_image(
1425                &source,
1426                &mut BooleanDecisionDiagram::default(),
1427                pair.into(),
1428                1,
1429            )
1430            .unwrap();
1431        }
1432        assert!(matches!(
1433            boolean_image(
1434                &source,
1435                &mut BooleanDecisionDiagram::default(),
1436                vec![(6, a), (1, not_a), (4, b), (8, not_b)],
1437                1,
1438            ),
1439            Err(BlockGraphError::Stabilizer(
1440                StabilizerError::ResourceLimited {
1441                    phase: "structural selector image",
1442                    observed: 2,
1443                    limit: 1,
1444                }
1445            ))
1446        ));
1447
1448        let parity = source.apply(BooleanOp::Xor, a, b).unwrap();
1449        assert_eq!(
1450            boolean_image(
1451                &source,
1452                &mut BooleanDecisionDiagram::default(),
1453                vec![(9, parity)],
1454                1,
1455            )
1456            .unwrap(),
1457            DECISION_TRUE
1458        );
1459        assert!(matches!(
1460            boolean_image(
1461                &source,
1462                &mut BooleanDecisionDiagram::default(),
1463                vec![(9, parity)],
1464                0,
1465            ),
1466            Err(BlockGraphError::Stabilizer(
1467                StabilizerError::ResourceLimited {
1468                    phase: "structural selector image",
1469                    observed: 1,
1470                    limit: 0,
1471                }
1472            ))
1473        ));
1474        let mut no_work = BooleanDecisionDiagram::with_limits(bloq_utils::boolean::BooleanLimits {
1475            max_nodes: 1,
1476            max_steps: 0,
1477        });
1478        assert!(matches!(
1479            boolean_image(&source, &mut no_work, vec![(9, parity)], 1),
1480            Err(BlockGraphError::Stabilizer(
1481                StabilizerError::ResourceLimited {
1482                    phase: "Boolean work steps",
1483                    ..
1484                }
1485            ))
1486        ));
1487    }
1488
1489    #[test]
1490    fn constant_case_coordinates_spend_work_before_enumeration() {
1491        let mut topology =
1492            GuardedTopology::new(&BlockGraph::new(), ModuleCertificationLimits::DEFAULT).unwrap();
1493        topology.diagram =
1494            BooleanDecisionDiagram::with_limits(bloq_utils::boolean::BooleanLimits {
1495                max_nodes: 0,
1496                max_steps: 1,
1497            });
1498        // Constant coordinates count even when no Boolean node is created.
1499        assert!(matches!(
1500            topology.cases(&[DECISION_TRUE], DECISION_TRUE),
1501            Err(BlockGraphError::Stabilizer(
1502                StabilizerError::ResourceLimited {
1503                    phase: "Boolean work steps",
1504                    observed: 2,
1505                    limit: 1,
1506                }
1507            ))
1508        ));
1509    }
1510
1511    #[test]
1512    fn local_cases_partition_enabled_correlated_values_exactly() {
1513        let mut topology =
1514            GuardedTopology::new(&BlockGraph::new(), ModuleCertificationLimits::DEFAULT).unwrap();
1515        let a = topology
1516            .diagram
1517            .make_node(0, DECISION_FALSE, DECISION_TRUE)
1518            .unwrap();
1519        let b = topology
1520            .diagram
1521            .make_node(1, DECISION_FALSE, DECISION_TRUE)
1522            .unwrap();
1523        let c = topology
1524            .diagram
1525            .make_node(2, DECISION_FALSE, DECISION_TRUE)
1526            .unwrap();
1527        let parity = topology.diagram.apply(BooleanOp::Xor, a, b).unwrap();
1528        let product = topology.diagram.apply(BooleanOp::And, b, c).unwrap();
1529        topology.domain = topology.diagram.apply(BooleanOp::Or, a, c).unwrap();
1530        let roots = [DECISION_TRUE, parity, product, a, parity];
1531        let cases = topology.cases(&roots, b).unwrap();
1532        for assignment in 0..8 {
1533            let evaluate = |root| {
1534                topology
1535                    .diagram
1536                    .evaluate(root, |index| assignment & (1 << index) != 0)
1537            };
1538            let enabled = evaluate(b) && evaluate(topology.domain);
1539            let selected = cases
1540                .iter()
1541                .filter(|(_, guard)| evaluate(*guard))
1542                .collect::<Vec<_>>();
1543            if evaluate(topology.domain) {
1544                assert_eq!(selected.len(), usize::from(enabled));
1545            }
1546            if enabled {
1547                assert_eq!(selected[0].0, roots.map(evaluate));
1548            }
1549        }
1550        topology.limits.max_guarded_domain_size = cases.len() - 1;
1551        assert!(matches!(
1552            topology.cases(&roots, b),
1553            Err(BlockGraphError::Stabilizer(
1554                StabilizerError::ResourceLimited {
1555                    phase: "local structural patterns",
1556                    ..
1557                }
1558            ))
1559        ));
1560        assert!(topology.cases(&roots, DECISION_FALSE).unwrap().is_empty());
1561    }
1562
1563    #[test]
1564    fn independent_neighborhoods_do_not_form_a_joint_projection_domain() {
1565        let mut source = String::from("BLOG 1.0\nmodule main {\n");
1566        for index in 0..40 {
1567            let x = index * 3;
1568            source.push_str(&format!("in s{index}\n{}: ZXZ [{x},0,0]\n{}: ZXZ [{x},0,2]\nbranch b{index} {{\nfalse {{\n{}: XZX [{x},0,1]\n[{x},0,0] -H> +Z\n[{x},0,1] -H> +Z\n}}\ntrue {{\n{}: ZXZ [{x},0,1]\n[{x},0,0] -> +Z\n[{x},0,1] -> +Z\n}}\n}}\nresolve b{index} if s{index}\n", index*4, index*4+1, index*4+2, index*4+3));
1569        }
1570        source.push_str("}\n");
1571        let ast = crate::parse_blog_program_to_ast(&source).unwrap();
1572        let program = crate::lower_blog_graph_ast_deferred(&ast).unwrap();
1573        let graph = program.materialize_flat_graph().unwrap();
1574        assert!(matches!(
1575            GuardedTopology::new(
1576                &graph,
1577                ModuleCertificationLimits {
1578                    max_boolean_nodes: 0,
1579                    ..ModuleCertificationLimits::DEFAULT
1580                }
1581            ),
1582            Err(BlockGraphError::Stabilizer(
1583                StabilizerError::ResourceLimited {
1584                    phase: "Boolean nodes",
1585                    observed: 1,
1586                    limit: 0,
1587                }
1588            ))
1589        ));
1590        assert!(matches!(
1591            GuardedTopology::new(&graph, ModuleCertificationLimits {
1592                    max_expanded_blocks: 2 * graph.block_count() - 1,
1593                ..ModuleCertificationLimits::DEFAULT
1594            }),
1595            Err(BlockGraphError::Stabilizer(StabilizerError::ResourceLimited {
1596                phase: "retained projection blocks", observed, limit,
1597            })) if observed > limit
1598        ));
1599        let mut topology = GuardedTopology::new(
1600            &graph,
1601            ModuleCertificationLimits {
1602                max_guarded_domain_size: 4,
1603                ..ModuleCertificationLimits::DEFAULT
1604            },
1605        )
1606        .unwrap();
1607        assert_eq!(topology.branches.len(), 40);
1608        let mut products = DECISION_FALSE;
1609        for index in 0..12 {
1610            let name = format!("s{index}");
1611            let outcome = topology.condition(&Expr::Var(name));
1612            let branch = topology.branches[index].2;
1613            assert_eq!(
1614                topology.diagram.node(branch).unwrap().variable,
1615                topology.diagram.node(outcome).unwrap().variable + 1,
1616            );
1617            let term = topology
1618                .diagram
1619                .apply(BooleanOp::And, outcome, branch)
1620                .unwrap();
1621            products = topology
1622                .diagram
1623                .apply(BooleanOp::Xor, products, term)
1624                .unwrap();
1625        }
1626        let mut compact = topology.diagram.clone();
1627        compact.collect_garbage([&mut products]);
1628        // Grouped outcome/branch variables need 10,237 live nodes here.
1629        assert_eq!(compact.nodes().len(), 46);
1630        assert_eq!(topology.projections.len(), 1);
1631        let reference = topology.project(DECISION_TRUE).unwrap();
1632        assert_eq!(
1633            topology.retained_projection_size[0],
1634            4 * graph.block_count()
1635        );
1636        for variants in topology.sites.values() {
1637            assert_eq!(variants.len(), 2);
1638            assert_eq!(
1639                variants
1640                    .iter()
1641                    .filter(|variant| Arc::ptr_eq(&variant.graph, &reference))
1642                    .count(),
1643                1
1644            );
1645            for variant in variants {
1646                if !Arc::ptr_eq(&variant.graph, &reference) {
1647                    assert_eq!(variant.graph.block_count(), 3);
1648                }
1649            }
1650        }
1651        assert_eq!(reference.block_count(), 120);
1652        assert!(Arc::ptr_eq(
1653            &reference,
1654            &topology.project(DECISION_TRUE).unwrap()
1655        ));
1656        assert_eq!(topology.projections.len(), 1);
1657        for _ in 0..2 {
1658            for index in 0..40 {
1659                let name = format!("s{index}");
1660                let condition = topology.condition(&Expr::Var(name.clone()));
1661                assert!(topology.evaluate_source(condition, |candidate: &str| candidate == name));
1662                assert!(!topology.evaluate_source(condition, |_: &str| false));
1663            }
1664            topology.collect_garbage(std::iter::empty());
1665        }
1666    }
1667
1668    #[test]
1669    fn compound_conditions_preserve_aliases_and_exact_shape_after_collection() {
1670        let var = |name: &str| Expr::Var(name.to_owned());
1671        let alias = Expr::Binary(crate::BinaryOp::Xor, Box::new(var("a")), Box::new(var("b")));
1672        let condition = Expr::Binary(
1673            crate::BinaryOp::Xor,
1674            Box::new(var("alias")),
1675            Box::new(Expr::Not(Box::new(var("a")))),
1676        );
1677        let mut graph = BlockGraph::new();
1678        graph
1679            .set_actions_with_inputs(
1680                vec![
1681                    Action::Let {
1682                        name: "alias".into(),
1683                        expr: alias,
1684                    },
1685                    Action::DiscardIf(condition.clone()),
1686                    Action::DiscardIf(condition.clone()),
1687                    Action::DiscardIf(var("alias")),
1688                    Action::DiscardIf(var("alias")),
1689                ],
1690                ["a", "b"].map(str::to_owned),
1691            )
1692            .unwrap();
1693        let mut topology =
1694            GuardedTopology::new(&graph, ModuleCertificationLimits::DEFAULT).unwrap();
1695        let before = topology.condition(&condition);
1696        for round in 0..2 {
1697            for values in 0..4 {
1698                let a = values & 1 != 0;
1699                let b = values & 2 != 0;
1700                let outcome = |name: &str| match name {
1701                    "a" => a,
1702                    "b" => b,
1703                    _ => panic!("unexpected source outcome {name}"),
1704                };
1705                assert_eq!(
1706                    topology.evaluate_source(topology.condition(&condition.clone()), outcome),
1707                    (a ^ b) ^ !a,
1708                );
1709                assert_eq!(
1710                    topology.evaluate_source(topology.condition(&var("alias")), outcome),
1711                    a ^ b,
1712                );
1713            }
1714            let removed = topology.collect_garbage(std::iter::empty());
1715            if round == 0 {
1716                assert!(removed > 0, "unused expression intermediates removed");
1717                assert_ne!(topology.condition(&condition), before, "root remapped");
1718            }
1719        }
1720        let absent = Expr::Binary(
1721            crate::BinaryOp::Xor,
1722            Box::new(Expr::Not(Box::new(var("a")))),
1723            Box::new(var("alias")),
1724        );
1725        assert!(
1726            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| topology.condition(&absent)))
1727                .is_err(),
1728            "equivalent reordered operands are not a stored source expression",
1729        );
1730    }
1731
1732    #[test]
1733    fn local_geometry_preserves_primary_kinds_and_complete_incidence() {
1734        for item in [
1735            crate::GalleryItem::CNOT,
1736            crate::GalleryItem::CZSpatialH,
1737            crate::GalleryItem::CCZGateTeleport,
1738            crate::GalleryItem::GHZPatchRotations,
1739            crate::GalleryItem::MoveRotation,
1740        ] {
1741            let source = item
1742                .build()
1743                .materialize_root_graph()
1744                .expect("gallery flat projection");
1745            let mut topology =
1746                GuardedTopology::new(&source, ModuleCertificationLimits::DEFAULT).unwrap();
1747            assert_eq!(topology.projections.len(), 1);
1748            let reference = topology.project(DECISION_TRUE).unwrap();
1749            let mut checked_assignments = std::collections::HashSet::new();
1750            for (center, variants) in topology.sites.clone() {
1751                let center = IVec3::from_array(center);
1752                for variant in variants {
1753                    let full = topology.project(variant.guard).unwrap();
1754                    for block in std::iter::once(full.get_block(center).unwrap())
1755                        .chain(full.neighbors(center))
1756                    {
1757                        assert_eq!(
1758                            variant.graph.get_block(block.pos()),
1759                            Some(block),
1760                            "{item}: center {center}, block {}",
1761                            block.pos(),
1762                        );
1763                        let pipes = |graph: &BlockGraph| {
1764                            graph
1765                                .pipes_at(block.pos())
1766                                .cloned()
1767                                .collect::<std::collections::HashSet<_>>()
1768                        };
1769                        assert_eq!(pipes(&variant.graph), pipes(&full), "{item}: {center}");
1770                    }
1771                    if !Arc::ptr_eq(&variant.graph, &reference) {
1772                        assert!(variant.graph.actions().is_empty());
1773                    }
1774                    assert!(variant.graph.branch_definitions().is_empty());
1775                    let assignment = topology.assignment(variant.guard).unwrap().unwrap();
1776                    if checked_assignments.insert(assignment.clone()) {
1777                        let reference = source.project_branches_deferred(assignment).unwrap();
1778                        assert_eq!(
1779                            full.blocks().collect::<std::collections::HashSet<_>>(),
1780                            reference.blocks().collect(),
1781                            "{item}",
1782                        );
1783                        assert_eq!(
1784                            full.pipes().collect::<std::collections::HashSet<_>>(),
1785                            reference.pipes().collect(),
1786                            "{item}",
1787                        );
1788                        assert_eq!(full.actions(), reference.actions(), "{item}");
1789                    }
1790                }
1791            }
1792        }
1793    }
1794
1795    #[test]
1796    fn full_projections_charge_selected_arms_before_materialization() {
1797        let source = "BLOG 1.0\nmodule main {\nin s\n0: ZXZ [0,0,0]\nbranch b {\nfalse {\n1: X [0,0,1]\n[0,0,0] -> +Z\n}\ntrue {\n2: ZXZ [0,0,1]\n3: ZXZ [0,0,2]\n[0,0,0] -> +Z\n[0,0,1] -> +Z\n}\n}\nresolve b if s\n}\n";
1798        let ast = crate::parse_blog_program_to_ast(source).unwrap();
1799        let program = crate::lower_blog_graph_ast_deferred(&ast).unwrap();
1800        let source = program.materialize_flat_graph().unwrap();
1801        for (blocks, cells, refused) in [
1802            (5, 5, None),
1803            (4, 5, Some("retained projection blocks")),
1804            (5, 4, Some("retained projection footprint cells")),
1805        ] {
1806            let limits = ModuleCertificationLimits {
1807                max_expanded_blocks: blocks,
1808                max_occupied_cells: cells,
1809                ..ModuleCertificationLimits::DEFAULT
1810            };
1811            let (mut topology, _) = GuardedTopology::new_with_geometry(&source, limits).unwrap();
1812            let reference = topology.project(DECISION_TRUE).unwrap();
1813            assert_eq!(reference.block_count(), 2);
1814            assert_eq!(topology.retained_projection_size, [2, 2]);
1815            let guard = topology.branches[0].2;
1816            if let Some(expected) = refused {
1817                assert!(matches!(
1818                    topology.project(guard),
1819                    Err(BlockGraphError::Stabilizer(StabilizerError::ResourceLimited {
1820                        phase,
1821                        observed: 5,
1822                        limit: 4,
1823                    })) if phase == expected
1824                ));
1825                assert_eq!(topology.projections.len(), 1);
1826                assert_eq!(topology.retained_projection_size, [2, 2]);
1827            } else {
1828                let selected = topology.project(guard).unwrap();
1829                assert_eq!(selected.block_count(), 3);
1830                assert_eq!(topology.retained_projection_size, [5, 5]);
1831                assert!(Arc::ptr_eq(&selected, &topology.project(guard).unwrap()));
1832                assert_eq!(topology.retained_projection_size, [5, 5]);
1833                assert_eq!(topology.projections.len(), 2);
1834            }
1835        }
1836    }
1837
1838    #[test]
1839    fn unchanged_sites_share_one_reference_including_source_actions() {
1840        let source = crate::GalleryItem::T
1841            .build()
1842            .materialize_root_graph()
1843            .expect("gallery flat projection");
1844        let mut topology =
1845            GuardedTopology::new(&source, ModuleCertificationLimits::DEFAULT).unwrap();
1846        let reference = topology.project(DECISION_TRUE).unwrap();
1847        assert!(!reference.actions().is_empty());
1848        assert_eq!(topology.projections.len(), 1);
1849        assert_eq!(
1850            topology.retained_projection_size,
1851            topology.common_projection_size
1852        );
1853        for (position, variants) in &topology.sites {
1854            for variant in variants {
1855                assert!(Arc::ptr_eq(&variant.graph, &reference));
1856                let local = crate::ZXGraph::from_local_block_neighborhood(
1857                    &variant.graph,
1858                    IVec3::from_array(*position),
1859                )
1860                .unwrap();
1861                assert!(local.actions().is_empty());
1862            }
1863        }
1864    }
1865}