1use 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 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#[must_use]
47pub fn selective_selector_name(position: IVec3) -> String {
48 format!("selective [{},{},{}]", position.x, position.y, position.z)
49}
50
51pub(crate) struct AvailabilityQuery {
55 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
119fn boolean_image(
122 source: &BooleanDecisionDiagram,
123 output: &mut BooleanDecisionDiagram,
124 roots: Vec<(usize, DecisionId)>,
125 limit: usize,
126) -> Result<DecisionId, BlockGraphError> {
127 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 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
207fn 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#[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[®ion.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 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 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#[doc(hidden)]
617#[derive(Debug)]
618pub struct GuardedTopology {
619 pub source: BlockGraph,
620 pub diagram: BooleanDecisionDiagram,
621 pub variables: Vec<GuardedVariable>,
622 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 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 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 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 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 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, ®ions, &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 topology.project(DECISION_TRUE)?;
922 Ok((topology, geometry))
923 }
924
925 fn populate_sites(&mut self, geometry: AuthoredGeometry) -> Result<(), BlockGraphError> {
930 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 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 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 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 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 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 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 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 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}