1use rustc_hash::{FxHashMap, FxHashSet};
3use std::collections::{BTreeMap, BTreeSet};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum BooleanOp {
8 Xor,
10 And,
12 Or,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct DecisionId(pub usize);
19pub const DECISION_FALSE: DecisionId = DecisionId(0);
21pub const DECISION_TRUE: DecisionId = DecisionId(1);
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub struct DecisionNode {
27 pub variable: usize,
29 pub low: DecisionId,
31 pub high: DecisionId,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct BooleanLimits {
38 pub max_nodes: usize,
40 pub max_steps: usize,
42}
43
44impl BooleanLimits {
45 pub const DEFAULT: Self = Self {
47 max_nodes: 4_000_000,
48 max_steps: 64_000_000,
49 };
50 pub const UNLIMITED: Self = Self {
52 max_nodes: usize::MAX,
53 max_steps: usize::MAX,
54 };
55}
56
57impl Default for BooleanLimits {
58 fn default() -> Self {
59 Self::DEFAULT
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
65#[error("{resource} requires {observed}, limit is {limit}")]
66pub struct BooleanResourceError {
67 pub resource: &'static str,
69 pub observed: usize,
71 pub limit: usize,
73}
74
75const MAX_CACHE_ENTRIES: usize = 262_144;
77
78#[derive(Debug, Clone, Default)]
80pub struct BooleanDecisionDiagram {
81 nodes: Vec<DecisionNode>,
82 unique: FxHashMap<DecisionNode, DecisionId>,
83 apply_cache: FxHashMap<(BooleanOp, DecisionId, DecisionId), DecisionId>,
84 not_cache: Vec<DecisionId>,
85 limits: BooleanLimits,
86 steps: usize,
87 row_witnesses: Option<RowWitnessTape>,
88}
89
90impl BooleanDecisionDiagram {
91 pub fn with_limits(limits: BooleanLimits) -> Self {
93 Self {
94 limits,
95 ..Self::default()
96 }
97 }
98
99 pub fn limits(&self) -> BooleanLimits {
101 self.limits
102 }
103
104 pub fn steps(&self) -> usize {
106 self.steps
107 }
108
109 pub fn charge(&mut self, steps: usize) -> Result<(), BooleanResourceError> {
118 let observed = self.steps.saturating_add(steps);
119 if steps > self.limits.max_steps.saturating_sub(self.steps) {
120 return Err(BooleanResourceError {
121 resource: "Boolean work steps",
122 observed,
123 limit: self.limits.max_steps,
124 });
125 }
126 self.steps = observed;
127 Ok(())
128 }
129
130 pub fn collect_garbage<'a>(
137 &mut self,
138 roots: impl IntoIterator<Item = &'a mut DecisionId>,
139 ) -> usize {
140 let mut roots = roots.into_iter().collect::<Vec<_>>();
141 if let Some(tape) = &mut self.row_witnesses {
142 roots.extend(tape.decisions_mut());
143 }
144 let mut live = vec![false; self.nodes.len()];
145 let mut pending = Vec::new();
146 for root in &roots {
147 pending.push(**root);
148 while let Some(root) = pending.pop() {
149 let Some(index) = root.0.checked_sub(2) else {
150 continue;
151 };
152 if !std::mem::replace(&mut live[index], true) {
153 let node = self.nodes[index];
154 pending.extend([node.low, node.high]);
155 }
156 }
157 }
158 let retained = live.iter().filter(|&&live| live).count();
159 let removed = self.nodes.len() - retained;
160 if removed == 0 {
161 self.apply_cache = FxHashMap::default();
162 return 0;
163 }
164 let mut mapping = vec![DECISION_FALSE; self.nodes.len() + 2];
165 mapping[1] = DECISION_TRUE;
166 let mut compact = Self {
167 nodes: Vec::with_capacity(retained),
168 unique: FxHashMap::with_capacity_and_hasher(retained, Default::default()),
169 not_cache: vec![DECISION_FALSE; retained],
170 limits: self.limits,
171 steps: self.steps,
172 ..Self::default()
173 };
174 for (index, node) in self
177 .nodes
178 .iter()
179 .enumerate()
180 .filter(|(index, _)| live[*index])
181 {
182 let node = DecisionNode {
183 variable: node.variable,
184 low: mapping[node.low.0],
185 high: mapping[node.high.0],
186 };
187 let id = DecisionId(compact.nodes.len() + 2);
188 mapping[index + 2] = id;
189 compact.nodes.push(node);
190 compact.unique.insert(node, id);
191 }
192 for (index, &negated) in self.not_cache.iter().enumerate() {
195 if live[index]
196 && let Some(other) = negated.0.checked_sub(2)
197 && live[other]
198 {
199 compact.not_cache[mapping[index + 2].0 - 2] = mapping[negated.0];
200 }
201 }
202 for root in roots {
203 *root = mapping[root.0];
204 }
205 compact.row_witnesses = self.row_witnesses.take();
206 *self = compact;
207 removed
208 }
209
210 pub fn evaluate(&self, mut root: DecisionId, mut variable: impl FnMut(usize) -> bool) -> bool {
212 while let Some(node) = self.node(root) {
213 root = if variable(node.variable) {
214 node.high
215 } else {
216 node.low
217 };
218 }
219 root == DECISION_TRUE
220 }
221
222 pub fn witness(&self, mut root: DecisionId) -> Option<BTreeMap<usize, bool>> {
224 if root == DECISION_FALSE {
225 return None;
226 }
227 let mut values = BTreeMap::new();
228 while let Some(node) = self.node(root) {
229 let high = node.low == DECISION_FALSE;
230 values.insert(node.variable, high);
231 root = if high { node.high } else { node.low };
232 }
233 Some(values)
234 }
235
236 pub fn conjunction_witness(
242 &mut self,
243 roots: &[DecisionId],
244 ) -> Result<Option<BTreeMap<usize, bool>>, BooleanResourceError> {
245 enum Task {
246 Visit(Vec<DecisionId>),
247 Retry(Vec<DecisionId>, usize),
248 Finish(Vec<DecisionId>, usize),
249 }
250 self.charge(roots.len().max(1))?;
251 let mut pending = vec![Task::Visit(roots.to_vec())];
252 let mut values = BTreeMap::new();
253 let mut dead = FxHashSet::default();
254 let mut cached_terms = 0;
255 while let Some(task) = pending.pop() {
256 match task {
257 Task::Visit(roots) => {
258 self.charge(roots.len().max(1))?;
259 if roots.contains(&DECISION_FALSE) || dead.contains(&roots) {
260 continue;
261 }
262 let Some(variable) = roots
263 .iter()
264 .filter_map(|&root| self.node(root).map(|node| node.variable))
265 .min()
266 else {
267 return Ok(Some(values));
268 };
269 self.charge(roots.len().max(1))?;
270 let next = self.cofactors(&roots, variable, false);
271 values.insert(variable, false);
272 pending.push(Task::Retry(roots, variable));
273 pending.push(Task::Visit(next));
274 }
275 Task::Retry(roots, variable) => {
276 self.charge(roots.len().max(1))?;
277 let next = self.cofactors(&roots, variable, true);
278 values.insert(variable, true);
279 pending.push(Task::Finish(roots, variable));
280 pending.push(Task::Visit(next));
281 }
282 Task::Finish(roots, variable) => {
283 values.remove(&variable);
284 self.charge(roots.len().max(1))?;
285 if cached_terms + roots.len() > MAX_CACHE_ENTRIES {
286 dead.clear();
287 cached_terms = 0;
288 }
289 if roots.len() <= MAX_CACHE_ENTRIES {
290 cached_terms += roots.len();
291 dead.insert(roots);
292 }
293 }
294 }
295 }
296 Ok(None)
297 }
298
299 fn cofactors(&self, roots: &[DecisionId], variable: usize, high: bool) -> Vec<DecisionId> {
300 roots
301 .iter()
302 .map(|&root| {
303 self.node(root)
304 .filter(|node| node.variable == variable)
305 .map_or(root, |node| if high { node.high } else { node.low })
306 })
307 .collect()
308 }
309
310 pub fn variables(&self, root: DecisionId) -> BTreeSet<usize> {
312 let mut variables = BTreeSet::new();
313 let mut visited = FxHashSet::default();
314 let mut pending = vec![root];
315 while let Some(value) = pending.pop() {
316 if visited.insert(value)
317 && let Some(node) = self.node(value)
318 {
319 variables.insert(node.variable);
320 pending.extend([node.low, node.high]);
321 }
322 }
323 variables
324 }
325
326 pub fn constrain(
337 &mut self,
338 root: DecisionId,
339 care: DecisionId,
340 ) -> Result<DecisionId, BooleanResourceError> {
341 if care == DECISION_FALSE {
342 return Ok(DECISION_FALSE);
343 }
344 if root.0 < 2 || care == DECISION_TRUE {
345 return Ok(root);
346 }
347 self.charge(1)?;
348 enum Task {
349 Visit(DecisionId, DecisionId),
350 Finish((DecisionId, DecisionId), Option<usize>),
351 }
352 let mut pending = vec![Task::Visit(root, care)];
353 let mut results = Vec::new();
354 let mut memo = FxHashMap::default();
355 while let Some(task) = pending.pop() {
356 match task {
357 Task::Visit(root, care) => {
358 if care == DECISION_FALSE {
359 results.push(DECISION_FALSE);
360 continue;
361 }
362 if root.0 < 2 || care == DECISION_TRUE {
363 results.push(root);
364 continue;
365 }
366 if let Some(&value) = memo.get(&(root, care)) {
367 results.push(value);
368 continue;
369 }
370 self.charge(1)?;
371 let a = self.node(root).expect("nonterminal root has a node");
372 let b = self.node(care).expect("nonterminal care has a node");
373 let variable = a.variable.min(b.variable);
374 let (al, ah) = Self::branches(a, root, variable);
375 let (bl, bh) = Self::branches(b, care, variable);
376 if bl == DECISION_FALSE {
377 pending.push(Task::Finish((root, care), None));
378 pending.push(Task::Visit(ah, bh));
379 } else if bh == DECISION_FALSE {
380 pending.push(Task::Finish((root, care), None));
381 pending.push(Task::Visit(al, bl));
382 } else {
383 pending.push(Task::Finish((root, care), Some(variable)));
384 pending.push(Task::Visit(ah, bh));
385 pending.push(Task::Visit(al, bl));
386 }
387 }
388 Task::Finish(key, variable) => {
389 let high = results.pop().expect("visited child has a result");
390 let value = if let Some(variable) = variable {
391 let low = results.pop().expect("visited child has a result");
392 self.make_node(variable, low, high)?
393 } else {
394 high
395 };
396 if memo.len() == MAX_CACHE_ENTRIES {
397 memo.clear();
398 }
399 memo.insert(key, value);
400 results.push(value);
401 }
402 }
403 }
404 Ok(results.pop().expect("root has a result"))
405 }
406
407 fn branches(node: DecisionNode, root: DecisionId, variable: usize) -> (DecisionId, DecisionId) {
408 if node.variable == variable {
409 (node.low, node.high)
410 } else {
411 (root, root)
412 }
413 }
414
415 pub fn nodes(&self) -> &[DecisionNode] {
417 &self.nodes
418 }
419 pub fn make_node(
425 &mut self,
426 variable: usize,
427 low: DecisionId,
428 high: DecisionId,
429 ) -> Result<DecisionId, BooleanResourceError> {
430 if low == high {
431 return Ok(low);
432 }
433 let node = DecisionNode {
434 variable,
435 low,
436 high,
437 };
438 if let Some(&id) = self.unique.get(&node) {
439 return Ok(id);
440 }
441 if self.nodes.len() >= self.limits.max_nodes {
442 return Err(BooleanResourceError {
443 resource: "Boolean nodes",
444 observed: self.nodes.len().saturating_add(1),
445 limit: self.limits.max_nodes,
446 });
447 }
448 self.charge(1)?;
449 let id = DecisionId(self.nodes.len() + 2);
450 self.nodes.push(node);
451 self.not_cache.push(DECISION_FALSE);
452 self.unique.insert(node, id);
453 Ok(id)
454 }
455
456 pub fn node(&self, id: DecisionId) -> Option<DecisionNode> {
462 id.0.checked_sub(2).map(|index| self.nodes[index])
463 }
464
465 pub fn cached_complement(&self, root: DecisionId) -> Option<DecisionId> {
472 match root {
473 DECISION_FALSE => Some(DECISION_TRUE),
474 DECISION_TRUE => Some(DECISION_FALSE),
475 _ => {
476 let result = self.not_cache[root.0 - 2];
477 (result != DECISION_FALSE).then_some(result)
478 }
479 }
480 }
481
482 pub fn negate(&mut self, root: DecisionId) -> Result<DecisionId, BooleanResourceError> {
492 if let Some(result) = self.cached_complement(root) {
493 return Ok(result);
494 }
495 self.charge(1)?;
496 let mut pending = vec![(root, false)];
497 while let Some((root, finish)) = pending.pop() {
498 if self.cached_complement(root).is_some() {
499 continue;
500 }
501 let node = self.node(root).expect("nonterminal decision has a node");
502 if finish {
503 let low = self
504 .cached_complement(node.low)
505 .expect("visited child is negated");
506 let high = self
507 .cached_complement(node.high)
508 .expect("visited child is negated");
509 let negated = self.make_node(node.variable, low, high)?;
510 self.not_cache[root.0 - 2] = negated;
511 self.not_cache[negated.0 - 2] = root;
512 } else {
513 self.charge(1)?;
514 pending.push((root, true));
515 pending.push((node.high, false));
516 pending.push((node.low, false));
517 }
518 }
519 Ok(self.cached_complement(root).expect("root is negated"))
520 }
521
522 #[inline]
528 pub fn apply(
529 &mut self,
530 operator: BooleanOp,
531 mut left: DecisionId,
532 mut right: DecisionId,
533 ) -> Result<DecisionId, BooleanResourceError> {
534 if left > right {
535 std::mem::swap(&mut left, &mut right);
536 }
537 if let Some(value) = self.simple_apply(operator, left, right)? {
538 return Ok(value);
539 }
540 self.apply_nodes(operator, left, right)
541 }
542
543 #[inline]
544 fn simple_apply(
545 &mut self,
546 operator: BooleanOp,
547 left: DecisionId,
548 right: DecisionId,
549 ) -> Result<Option<DecisionId>, BooleanResourceError> {
550 use BooleanOp::{And, Or, Xor};
551 Ok(match operator {
552 Xor if left == DECISION_FALSE => Some(right),
553 Xor if left == right => Some(DECISION_FALSE),
554 Xor if left == DECISION_TRUE => Some(self.negate(right)?),
555 And if left == DECISION_FALSE || left == right => Some(left),
556 And if left == DECISION_TRUE => Some(right),
557 Or if left == DECISION_FALSE || left == right => Some(right),
558 Or if left == DECISION_TRUE => Some(left),
559 Xor | And | Or => None,
560 })
561 }
562
563 fn apply_nodes(
564 &mut self,
565 operator: BooleanOp,
566 left: DecisionId,
567 right: DecisionId,
568 ) -> Result<DecisionId, BooleanResourceError> {
569 if let Some(&result) = self.apply_cache.get(&(operator, left, right)) {
570 return Ok(result);
571 }
572 self.charge(1)?;
573 enum Task {
574 Visit(DecisionId, DecisionId),
575 Finish((BooleanOp, DecisionId, DecisionId), usize),
576 }
577 let mut pending = vec![Task::Visit(left, right)];
578 let mut results = Vec::new();
579 while let Some(task) = pending.pop() {
580 match task {
581 Task::Visit(mut left, mut right) => {
582 if left > right {
583 std::mem::swap(&mut left, &mut right);
584 }
585 if let Some(value) = self.simple_apply(operator, left, right)? {
586 results.push(value);
587 continue;
588 }
589 let key = (operator, left, right);
590 if let Some(&value) = self.apply_cache.get(&key) {
591 results.push(value);
592 continue;
593 }
594 self.charge(1)?;
595 let a = self.node(left).expect("terminal cases were simplified");
596 let b = self.node(right).expect("terminal cases were simplified");
597 let variable = a.variable.min(b.variable);
598 let (al, ah) = Self::branches(a, left, variable);
599 let (bl, bh) = Self::branches(b, right, variable);
600 pending.push(Task::Finish(key, variable));
601 pending.push(Task::Visit(ah, bh));
602 pending.push(Task::Visit(al, bl));
603 }
604 Task::Finish(key, variable) => {
605 let high = results.pop().expect("visited child has a result");
606 let low = results.pop().expect("visited child has a result");
607 let result = self.make_node(variable, low, high)?;
608 if self.apply_cache.len() == MAX_CACHE_ENTRIES {
609 self.apply_cache.clear();
610 }
611 self.apply_cache.insert(key, result);
612 results.push(result);
613 }
614 }
615 }
616 Ok(results.pop().expect("root has a result"))
617 }
618
619 pub fn values_up_to(
626 &mut self,
627 roots: &[DecisionId],
628 limit: usize,
629 ) -> Result<Vec<Vec<bool>>, BooleanResourceError> {
630 if limit == 0 {
631 return Ok(Vec::new());
632 }
633 self.charge(roots.len().max(1))?;
634 let mut reachable = BTreeSet::new();
635 let mut seen = FxHashSet::default();
636 let mut cached_terms = 0;
637 let mut pending = vec![roots.to_vec()];
638 while let Some(state) = pending.pop() {
639 self.charge(state.len().max(1))?;
640 if seen.contains(&state) {
641 continue;
642 }
643 if cached_terms + state.len() > MAX_CACHE_ENTRIES {
644 seen.clear();
645 cached_terms = 0;
646 }
647 if state.len() <= MAX_CACHE_ENTRIES {
648 self.charge(state.len().max(1))?;
649 cached_terms += state.len();
650 seen.insert(state.clone());
651 }
652 let variable = state
653 .iter()
654 .filter_map(|&root| self.node(root).map(|node| node.variable))
655 .min();
656 let Some(variable) = variable else {
657 self.charge(state.len().max(1))?;
658 reachable.insert(
659 state
660 .into_iter()
661 .map(|root| root == DECISION_TRUE)
662 .collect(),
663 );
664 if reachable.len() == limit {
665 break;
666 }
667 continue;
668 };
669 for high in [false, true] {
670 self.charge(state.len().max(1))?;
671 pending.push(self.cofactors(&state, variable, high));
672 }
673 }
674 Ok(reachable.into_iter().collect())
675 }
676}
677
678#[derive(Debug, Clone)]
681pub struct BooleanRow {
682 pub active: DecisionId,
684 bits: Vec<(usize, DecisionId)>,
687 witness: RowWitness,
688}
689
690#[derive(Debug, Clone, Copy, PartialEq, Eq)]
691enum RowWitness {
692 Explicit,
693 Deferred(Option<usize>),
694}
695
696#[derive(Debug, Clone)]
697enum RowWitnessOp {
698 Leaf(Vec<(usize, DecisionId)>),
699 Xor {
700 left: Option<usize>,
701 right: usize,
702 factor: DecisionId,
703 },
704}
705
706#[derive(Debug, Clone)]
707struct RowWitnessNode {
708 op: RowWitnessOp,
709 columns: (usize, usize),
711}
712
713#[derive(Debug, Clone)]
714struct RowWitnessTape {
715 explicit_columns: FxHashSet<usize>,
716 initial_explicit_columns: usize,
717 ops: Vec<RowWitnessNode>,
718 limit: usize,
719 roots: usize,
720}
721
722impl RowWitnessTape {
723 fn insertion_work(&self) -> Result<usize, BooleanResourceError> {
724 if self.ops.len() >= self.limit {
725 return Err(BooleanResourceError {
726 resource: "row witness operations",
727 observed: self.ops.len().saturating_add(1),
728 limit: self.limit,
729 });
730 }
731 Ok(1 + if self.ops.len() == self.ops.capacity() {
732 self.ops.len()
733 } else {
734 0
735 })
736 }
737
738 fn push(&mut self, op: RowWitnessOp) -> usize {
740 debug_assert!(self.ops.len() < self.limit);
741 let columns = match &op {
742 RowWitnessOp::Leaf(terms) => (
743 terms.first().expect("witness leaves are nonempty").0,
744 terms.last().expect("witness leaves are nonempty").0,
745 ),
746 RowWitnessOp::Xor { left, right, .. } => {
747 let (first, last) = self.ops[*right].columns;
748 left.map_or((first, last), |left| {
749 let (a, b) = self.ops[left].columns;
750 (a.min(first), b.max(last))
751 })
752 }
753 };
754 self.roots += match &op {
755 RowWitnessOp::Leaf(terms) => terms.len(),
756 RowWitnessOp::Xor { .. } => 1,
757 };
758 let id = self.ops.len();
759 self.ops.push(RowWitnessNode { op, columns });
760 id
761 }
762
763 fn decisions_mut(&mut self) -> impl Iterator<Item = &mut DecisionId> {
764 self.ops.iter_mut().flat_map(|node| {
765 let (terms, factor) = match &mut node.op {
766 RowWitnessOp::Leaf(terms) => (Some(terms), None),
767 RowWitnessOp::Xor { factor, .. } => (None, Some(factor)),
768 };
769 terms
770 .into_iter()
771 .flatten()
772 .map(|(_, root)| root)
773 .chain(factor)
774 })
775 }
776
777 fn reconstruct(
778 &self,
779 root: usize,
780 requested: Option<&[usize]>,
781 diagram: &mut BooleanDecisionDiagram,
782 ) -> Result<Vec<(usize, DecisionId)>, BooleanResourceError> {
783 diagram.charge(1)?;
784 let extended = self.explicit_columns.len() != self.initial_explicit_columns;
785 let mut pending = BTreeMap::from([(root, DECISION_TRUE)]);
790 let mut result = BTreeMap::<usize, DecisionId>::new();
791 while let Some((id, weight)) = pending.pop_last() {
792 diagram.charge(1)?;
793 if weight == DECISION_FALSE {
794 continue;
795 }
796 let node = &self.ops[id];
797 let requested = if let Some(columns) = requested {
798 diagram.charge(2 * (columns.len().max(1).ilog2() as usize + 1))?;
799 let first = columns.partition_point(|&column| column < node.columns.0);
800 let last = columns.partition_point(|&column| column <= node.columns.1);
801 if first == last {
802 continue;
803 }
804 Some(&columns[first..last])
805 } else {
806 None
807 };
808 match &node.op {
809 RowWitnessOp::Leaf(terms) => {
810 let mut accumulate = |column, value, diagram: &mut BooleanDecisionDiagram| {
811 if extended && self.explicit_columns.contains(&column) {
812 return Ok(());
813 }
814 let value = diagram.apply(BooleanOp::And, weight, value)?;
815 let previous = result.entry(column).or_insert(DECISION_FALSE);
816 *previous = diagram.apply(BooleanOp::Xor, *previous, value)?;
817 Ok::<_, BooleanResourceError>(())
818 };
819 if let Some(columns) = requested {
820 diagram.charge(
821 columns
822 .len()
823 .saturating_mul(terms.len().ilog2() as usize + 1),
824 )?;
825 for &column in columns {
826 if let Ok(index) = terms.binary_search_by_key(&column, |term| term.0) {
827 accumulate(column, terms[index].1, diagram)?;
828 }
829 }
830 } else {
831 diagram.charge(terms.len())?;
832 for &(column, value) in terms {
833 accumulate(column, value, diagram)?;
834 }
835 }
836 }
837 RowWitnessOp::Xor {
838 left,
839 right,
840 factor,
841 } => {
842 let scaled = diagram.apply(BooleanOp::And, weight, *factor)?;
843 for (child, value) in left
844 .map(|id| (id, weight))
845 .into_iter()
846 .chain(std::iter::once((*right, scaled)))
847 {
848 if value == DECISION_FALSE {
849 continue;
850 }
851 diagram.charge(1)?;
852 let previous = pending.entry(child).or_insert(DECISION_FALSE);
853 *previous = diagram.apply(BooleanOp::Xor, *previous, value)?;
854 }
855 }
856 }
857 }
858 diagram.charge(result.len())?;
859 let mut terms = Vec::with_capacity(result.len());
860 terms.extend(
861 result
862 .into_iter()
863 .filter(|(_, value)| *value != DECISION_FALSE),
864 );
865 Ok(terms)
866 }
867
868 fn project_roots(
872 &self,
873 roots: &[Option<usize>],
874 columns: &[usize],
875 diagram: &mut BooleanDecisionDiagram,
876 ) -> Result<Vec<Vec<(usize, DecisionId)>>, BooleanResourceError> {
877 diagram.charge(
878 self.ops
879 .len()
880 .saturating_mul(4)
881 .saturating_add(roots.len().saturating_mul(2)),
882 )?;
883 let mut needed = vec![false; self.ops.len()];
884 let mut subscribers = FxHashMap::<usize, Vec<usize>>::default();
885 for (index, &root) in roots.iter().enumerate() {
886 if let Some(root) = root {
887 needed[root] = true;
888 diagram.charge(1)?;
889 subscribers.entry(root).or_default().push(index);
890 }
891 }
892 let mut parents = vec![Vec::new(); self.ops.len()];
893 for (id, node) in self.ops.iter().enumerate().rev() {
894 if needed[id]
895 && let RowWitnessOp::Xor {
896 left,
897 right,
898 factor,
899 } = node.op
900 {
901 for (child, scale) in left
902 .map(|left| (left, DECISION_TRUE))
903 .into_iter()
904 .chain(std::iter::once((right, factor)))
905 {
906 diagram.charge(
907 1 + if parents[child].len() == parents[child].capacity() {
908 parents[child].len()
909 } else {
910 0
911 },
912 )?;
913 needed[child] = true;
914 parents[child].push((id, scale));
915 }
916 }
917 }
918 let mut seeds = BTreeMap::<usize, Vec<(usize, DecisionId)>>::new();
919 for (id, node) in self.ops.iter().enumerate() {
920 let RowWitnessOp::Leaf(terms) = &node.op else {
921 continue;
922 };
923 if !needed[id] {
924 continue;
925 }
926 diagram.charge(2 * (columns.len().max(1).ilog2() as usize + 1))?;
927 let first = columns.partition_point(|&column| column < node.columns.0);
928 let last = columns.partition_point(|&column| column <= node.columns.1);
929 let requested = &columns[first..last];
930 let mut seed = |column, value, diagram: &mut BooleanDecisionDiagram| {
931 diagram.charge(seeds.len().max(1).ilog2() as usize + 1)?;
932 let entries = seeds.entry(column).or_default();
933 diagram.charge(
934 1 + if entries.len() == entries.capacity() {
935 entries.len()
936 } else {
937 0
938 },
939 )?;
940 entries.push((id, value));
941 Ok::<_, BooleanResourceError>(())
942 };
943 if requested.len() <= terms.len() {
944 diagram.charge(
945 requested
946 .len()
947 .saturating_mul(terms.len().ilog2() as usize + 1),
948 )?;
949 for &column in requested {
950 if let Ok(index) = terms.binary_search_by_key(&column, |term| term.0) {
951 seed(column, terms[index].1, diagram)?;
952 }
953 }
954 } else {
955 diagram.charge(
956 terms
957 .len()
958 .saturating_mul(requested.len().ilog2() as usize + 1),
959 )?;
960 for &(column, value) in terms {
961 if requested.binary_search(&column).is_ok() {
962 seed(column, value, diagram)?;
963 }
964 }
965 }
966 }
967 let mut result = vec![Vec::new(); roots.len()];
968 for (column, leaves) in seeds {
969 diagram.charge(
970 leaves
971 .len()
972 .saturating_mul(leaves.len().max(1).ilog2() as usize + 1),
973 )?;
974 let mut pending = leaves.into_iter().collect::<BTreeMap<_, _>>();
975 while !pending.is_empty() {
976 diagram.charge(pending.len().ilog2() as usize + 1)?;
977 let (id, value) = pending.pop_first().expect("nonempty query worklist");
978 if value == DECISION_FALSE {
979 continue;
980 }
981 if let Some(rows) = subscribers.get(&id) {
982 diagram.charge(rows.len())?;
983 for &index in rows {
984 let front = &mut result[index];
985 diagram.charge(
986 1 + if front.len() == front.capacity() {
987 front.len()
988 } else {
989 0
990 },
991 )?;
992 front.push((column, value));
993 }
994 }
995 diagram.charge(parents[id].len())?;
996 for &(parent, scale) in &parents[id] {
997 let contribution = diagram.apply(BooleanOp::And, value, scale)?;
998 if contribution != DECISION_FALSE {
999 diagram.charge(pending.len().max(1).ilog2() as usize + 1)?;
1000 let previous = pending.entry(parent).or_insert(DECISION_FALSE);
1001 *previous = diagram.apply(BooleanOp::Xor, *previous, contribution)?;
1002 }
1003 }
1004 }
1005 }
1006 Ok(result)
1007 }
1008}
1009
1010impl BooleanDecisionDiagram {
1011 pub fn start_witness_tape(
1025 &mut self,
1026 explicit_columns: impl IntoIterator<Item = usize>,
1027 max_operations: usize,
1028 ) -> Result<(), BooleanResourceError> {
1029 assert!(
1030 self.row_witnesses.is_none(),
1031 "one tape owns this arena's rows"
1032 );
1033 let mut columns = FxHashSet::default();
1034 for column in explicit_columns {
1035 self.charge(1)?;
1036 if columns.len() == columns.capacity() && !columns.contains(&column) {
1037 self.charge(columns.len())?;
1038 }
1039 columns.insert(column);
1040 }
1041 self.row_witnesses = Some(RowWitnessTape {
1042 initial_explicit_columns: columns.len(),
1043 explicit_columns: columns,
1044 ops: Vec::new(),
1045 limit: max_operations,
1046 roots: 0,
1047 });
1048 Ok(())
1049 }
1050
1051 pub fn extend_witness_front(
1072 &mut self,
1073 columns: &[usize],
1074 rows: &mut [BooleanRow],
1075 ) -> Result<(), BooleanResourceError> {
1076 self.row_witnesses
1077 .as_ref()
1078 .expect("row witness tape started");
1079 self.charge(
1080 columns
1081 .len()
1082 .saturating_mul(columns.len().max(1).ilog2() as usize + 2)
1083 .saturating_add(rows.len()),
1084 )?;
1085 let mut columns = columns.to_vec();
1086 columns.sort_unstable();
1087 columns.dedup();
1088 let tape = self.row_witnesses.as_ref().expect("tape remains active");
1089 columns.retain(|column| !tape.explicit_columns.contains(column));
1090 if columns.is_empty() {
1091 return Ok(());
1092 }
1093
1094 self.charge(rows.len())?;
1095 let roots = rows
1096 .iter()
1097 .map(|row| match row.witness {
1098 RowWitness::Explicit => None,
1099 RowWitness::Deferred(root) => root,
1100 })
1101 .collect::<Vec<_>>();
1102 let tape = self.row_witnesses.take().expect("tape remains active");
1103 let projections = tape.project_roots(&roots, &columns, self);
1104 self.row_witnesses = Some(tape);
1105 let projections = projections?;
1106 let mut staged = Vec::with_capacity(rows.len());
1107 for (row, values) in rows.iter().zip(projections) {
1108 if row.witness == RowWitness::Explicit {
1109 staged.push(None);
1110 continue;
1111 }
1112 let capacity = row.bits.len().saturating_add(values.len());
1113 self.charge(capacity)?;
1114 let mut merged = Vec::with_capacity(capacity);
1115 let mut previous = row.bits.iter().copied().peekable();
1116 for (column, value) in values {
1117 while previous.peek().is_some_and(|&(old, _)| old < column) {
1118 merged.push(previous.next().expect("peek found a preceding term"));
1119 }
1120 debug_assert!(previous.peek().is_none_or(|&(old, _)| old != column));
1121 merged.push((column, value));
1122 }
1123 merged.extend(previous);
1124 staged.push(Some(merged));
1125 }
1126
1127 let capacity = self
1128 .row_witnesses
1129 .as_ref()
1130 .expect("queries retain the witness tape")
1131 .explicit_columns
1132 .len()
1133 .saturating_add(columns.len());
1134 self.charge(capacity)?;
1135 let tape = self.row_witnesses.as_mut().expect("tape remains active");
1136 let mut partition = FxHashSet::with_capacity_and_hasher(capacity, Default::default());
1137 partition.extend(tape.explicit_columns.iter().copied());
1138 partition.extend(columns);
1139 tape.explicit_columns = partition;
1140 for (row, front) in rows.iter_mut().zip(staged) {
1141 if let Some(front) = front {
1142 row.bits = front;
1143 }
1144 }
1145 Ok(())
1146 }
1147
1148 pub fn witness_collection_work(&self) -> usize {
1151 self.row_witnesses
1152 .as_ref()
1153 .map_or(0, |tape| tape.ops.len().saturating_add(tape.roots))
1154 }
1155
1156 pub fn finish_witness_tape(&mut self) {
1158 self.row_witnesses = None;
1159 }
1160
1161 fn xor_witness(
1162 &mut self,
1163 left: Option<usize>,
1164 right: Option<usize>,
1165 factor: DecisionId,
1166 ) -> Result<Option<usize>, BooleanResourceError> {
1167 self.charge(1)?;
1168 let tape = self
1169 .row_witnesses
1170 .as_ref()
1171 .expect("deferred row has an owned tape");
1172 let Some(right) = right else { return Ok(left) };
1173 if factor == DECISION_TRUE {
1174 if left == Some(right) {
1175 return Ok(None);
1176 }
1177 if left.is_none() {
1178 return Ok(Some(right));
1179 }
1180 }
1181 let work = tape.insertion_work()?;
1182 self.charge(work)?;
1183 let tape = self
1184 .row_witnesses
1185 .as_mut()
1186 .expect("charging work retains the owned witness tape");
1187 Ok(Some(tape.push(RowWitnessOp::Xor {
1188 left,
1189 right,
1190 factor,
1191 })))
1192 }
1193}
1194
1195impl BooleanRow {
1196 pub fn probe_coefficients(
1212 &self,
1213 columns: &[usize],
1214 diagram: &mut BooleanDecisionDiagram,
1215 ) -> Result<Vec<DecisionId>, BooleanResourceError> {
1216 diagram.charge(
1217 columns
1218 .len()
1219 .saturating_mul(self.bits.len().max(1).ilog2() as usize + 1)
1220 .saturating_add(1),
1221 )?;
1222 let mut result = columns
1223 .iter()
1224 .map(|&column| self.get(column))
1225 .collect::<Vec<_>>();
1226 let RowWitness::Deferred(Some(root)) = self.witness else {
1227 return Ok(result);
1228 };
1229 if columns.is_empty() {
1230 return Ok(result);
1231 }
1232 diagram.charge(
1233 columns
1234 .len()
1235 .saturating_mul(columns.len().ilog2() as usize + 2),
1236 )?;
1237 let mut requested = columns.to_vec();
1238 requested.sort_unstable();
1239 requested.dedup();
1240 diagram.charge(requested.len())?;
1241 let tape = diagram
1242 .row_witnesses
1243 .as_ref()
1244 .expect("deferred row has an owned witness tape");
1245 requested.retain(|column| !tape.explicit_columns.contains(column));
1246 if requested.is_empty() {
1247 return Ok(result);
1248 }
1249 let tape = diagram
1250 .row_witnesses
1251 .take()
1252 .expect("deferred row has an owned witness tape");
1253 let passive = tape.reconstruct(root, Some(&requested), diagram);
1254 diagram.row_witnesses = Some(tape);
1255 let passive = passive?;
1256 diagram.charge(
1257 columns
1258 .len()
1259 .saturating_mul(passive.len().max(1).ilog2() as usize + 1),
1260 )?;
1261 for (&column, value) in columns.iter().zip(&mut result) {
1262 if let Ok(index) = passive.binary_search_by_key(&column, |term| term.0) {
1263 debug_assert_eq!(
1264 *value, DECISION_FALSE,
1265 "explicit and passive columns are disjoint"
1266 );
1267 *value = passive[index].1;
1268 }
1269 }
1270 Ok(result)
1271 }
1272
1273 pub fn defer_coefficients(
1285 &mut self,
1286 diagram: &mut BooleanDecisionDiagram,
1287 ) -> Result<(), BooleanResourceError> {
1288 assert_eq!(
1289 self.witness,
1290 RowWitness::Explicit,
1291 "row is already deferred"
1292 );
1293 diagram.charge(self.bits.len().saturating_mul(2).saturating_add(1))?;
1294 let tape = diagram
1295 .row_witnesses
1296 .as_ref()
1297 .expect("row witness tape started");
1298 let passive_len = self
1299 .bits
1300 .iter()
1301 .filter(|(column, _)| !tape.explicit_columns.contains(column))
1302 .count();
1303 if passive_len != 0 {
1304 let work = tape.insertion_work()?;
1305 diagram.charge(work)?;
1306 }
1307 let mut core = Vec::with_capacity(self.bits.len() - passive_len);
1308 let mut passive = Vec::with_capacity(passive_len);
1309 let tape = diagram
1310 .row_witnesses
1311 .as_mut()
1312 .expect("charging work retains the started witness tape");
1313 for &term in &self.bits {
1314 if tape.explicit_columns.contains(&term.0) {
1315 core.push(term);
1316 } else {
1317 passive.push(term);
1318 }
1319 }
1320 let witness = if passive.is_empty() {
1321 None
1322 } else {
1323 Some(tape.push(RowWitnessOp::Leaf(passive)))
1324 };
1325 self.bits = core;
1326 self.witness = RowWitness::Deferred(witness);
1327 Ok(())
1328 }
1329
1330 pub fn expand_coefficients(
1342 &mut self,
1343 diagram: &mut BooleanDecisionDiagram,
1344 ) -> Result<(), BooleanResourceError> {
1345 let RowWitness::Deferred(root) = self.witness else {
1346 return Ok(());
1347 };
1348 let Some(root) = root else {
1349 self.witness = RowWitness::Explicit;
1350 return Ok(());
1351 };
1352 let tape = diagram
1353 .row_witnesses
1354 .take()
1355 .expect("row witness tape started");
1356 let expanded = tape.reconstruct(root, None, diagram);
1357 diagram.row_witnesses = Some(tape);
1358 let passive = expanded?;
1359 diagram.charge(self.bits.len().saturating_add(passive.len()))?;
1360 let mut merged = Vec::with_capacity(self.bits.len() + passive.len());
1361 let mut core = std::mem::take(&mut self.bits).into_iter().peekable();
1362 for term in passive {
1363 while core.peek().is_some_and(|&(column, _)| column < term.0) {
1364 merged.push(core.next().expect("peek found a preceding core term"));
1365 }
1366 debug_assert!(core.peek().is_none_or(|&(column, _)| column != term.0));
1367 merged.push(term);
1368 }
1369 merged.extend(core);
1370 self.bits = merged;
1371 self.witness = RowWitness::Explicit;
1372 Ok(())
1373 }
1374}
1375
1376impl Default for BooleanRow {
1377 fn default() -> Self {
1378 Self::new(DECISION_TRUE)
1379 }
1380}
1381
1382impl BooleanRow {
1383 pub fn new(active: DecisionId) -> Self {
1385 Self {
1386 active,
1387 bits: Vec::new(),
1388 witness: RowWitness::Explicit,
1389 }
1390 }
1391
1392 pub fn from_terms(
1394 active: DecisionId,
1395 terms: impl IntoIterator<Item = (usize, DecisionId)>,
1396 ) -> Self {
1397 let mut row = Self::new(active);
1398 for (column, value) in terms {
1399 row.set(column, value);
1400 }
1401 row
1402 }
1403
1404 pub fn terms(&self) -> &[(usize, DecisionId)] {
1407 &self.bits
1408 }
1409
1410 pub fn into_terms(self) -> Vec<(usize, DecisionId)> {
1416 assert_eq!(
1417 self.witness,
1418 RowWitness::Explicit,
1419 "expand deferred coefficients before consuming terms"
1420 );
1421 self.bits
1422 }
1423
1424 pub fn map_coefficients(
1434 &mut self,
1435 mut map: impl FnMut(DecisionId) -> Result<DecisionId, BooleanResourceError>,
1436 ) -> Result<(), BooleanResourceError> {
1437 assert_eq!(
1438 self.witness,
1439 RowWitness::Explicit,
1440 "expand deferred coefficients before mapping"
1441 );
1442 let mut result = Ok(());
1443 self.bits.retain_mut(|(_, value)| {
1444 if result.is_ok() {
1445 match map(*value) {
1446 Ok(mapped) => *value = mapped,
1447 Err(error) => result = Err(error),
1448 }
1449 }
1450 *value != DECISION_FALSE
1451 });
1452 result
1453 }
1454
1455 pub fn truncate_columns(&mut self, end: usize) {
1461 assert_eq!(
1462 self.witness,
1463 RowWitness::Explicit,
1464 "expand deferred coefficients before truncation"
1465 );
1466 self.bits
1467 .truncate(self.bits.partition_point(|&(column, _)| column < end));
1468 }
1469
1470 pub fn decisions_mut(&mut self) -> impl Iterator<Item = &mut DecisionId> {
1472 std::iter::once(&mut self.active).chain(self.bits.iter_mut().map(|(_, value)| value))
1473 }
1474
1475 pub fn get(&self, column: usize) -> DecisionId {
1477 self.bits
1478 .binary_search_by_key(&column, |&(column, _)| column)
1479 .map_or(DECISION_FALSE, |index| self.bits[index].1)
1480 }
1481
1482 pub fn set(&mut self, column: usize, value: DecisionId) {
1488 assert_eq!(
1489 self.witness,
1490 RowWitness::Explicit,
1491 "deferred rows use row operations only"
1492 );
1493 if self.bits.last().is_none_or(|&(last, _)| last < column) {
1494 if value != DECISION_FALSE {
1495 self.bits.push((column, value));
1496 }
1497 return;
1498 }
1499 match self
1500 .bits
1501 .binary_search_by_key(&column, |&(column, _)| column)
1502 {
1503 Ok(index) if value == DECISION_FALSE => {
1504 self.bits.remove(index);
1505 }
1506 Ok(index) => self.bits[index].1 = value,
1507 Err(index) if value != DECISION_FALSE => self.bits.insert(index, (column, value)),
1508 Err(_) => {}
1509 }
1510 }
1511
1512 pub fn xor_scaled(
1522 &mut self,
1523 other: &Self,
1524 factor: DecisionId,
1525 diagram: &mut BooleanDecisionDiagram,
1526 ) -> Result<(), BooleanResourceError> {
1527 if factor == DECISION_FALSE {
1528 return Ok(());
1529 }
1530 let witness = match (self.witness, other.witness) {
1531 (RowWitness::Explicit, RowWitness::Explicit) => RowWitness::Explicit,
1532 (RowWitness::Explicit, RowWitness::Deferred(right)) => {
1533 assert!(
1534 self.bits.is_empty(),
1535 "cannot mix untracked coefficients with a deferred row"
1536 );
1537 RowWitness::Deferred(diagram.xor_witness(None, right, factor)?)
1538 }
1539 (RowWitness::Deferred(left), RowWitness::Explicit) => {
1540 assert!(
1541 other.bits.is_empty(),
1542 "cannot mix untracked coefficients with a deferred row"
1543 );
1544 RowWitness::Deferred(left)
1545 }
1546 (RowWitness::Deferred(left), RowWitness::Deferred(right)) => {
1547 RowWitness::Deferred(diagram.xor_witness(left, right, factor)?)
1548 }
1549 };
1550 if other.bits.is_empty() {
1551 self.witness = witness;
1552 return Ok(());
1553 }
1554 if self.bits.is_empty() && factor == DECISION_TRUE {
1555 diagram.charge(other.bits.len())?;
1556 self.bits.clone_from(&other.bits);
1557 self.witness = witness;
1558 return Ok(());
1559 }
1560 let first = other
1563 .bits
1564 .first()
1565 .expect("empty source rows returned above")
1566 .0;
1567 let last = other
1568 .bits
1569 .last()
1570 .expect("empty source rows returned above")
1571 .0;
1572 let start = self.bits.partition_point(|&(column, _)| column < first);
1573 let end = self.bits.partition_point(|&(column, _)| column <= last);
1574 diagram.charge(other.bits.len().saturating_add(end - start))?;
1575 let mut merged = Vec::with_capacity((end - start).max(other.bits.len()));
1576 let mut left = self.bits[start..end].iter().peekable();
1577 for &(column, value) in &other.bits {
1578 while left.peek().is_some_and(|&&(current, _)| current < column) {
1579 merged.push(*left.next().expect("peek found a preceding left term"));
1580 }
1581 let current = if left.peek().is_some_and(|&&(current, _)| current == column) {
1582 left.next().expect("peek found the matching left term").1
1583 } else {
1584 DECISION_FALSE
1585 };
1586 let term = diagram.apply(BooleanOp::And, factor, value)?;
1587 let value = diagram.apply(BooleanOp::Xor, current, term)?;
1588 if value != DECISION_FALSE {
1589 merged.push((column, value));
1590 }
1591 }
1592 merged.extend(left);
1593 if merged.len() != end - start {
1594 diagram.charge(self.bits.len() - end)?;
1595 if self.bits.len() - (end - start) + merged.len() > self.bits.capacity() {
1596 diagram.charge(self.bits.len())?;
1597 }
1598 }
1599 drop(self.bits.splice(start..end, merged));
1600 self.witness = witness;
1601 Ok(())
1602 }
1603}
1604
1605#[derive(Debug, Default)]
1611pub struct BooleanRowSpace {
1612 rows: Vec<Option<BooleanRow>>,
1613 columns: FxHashMap<usize, BTreeSet<usize>>,
1614 live: usize,
1615}
1616
1617impl BooleanRowSpace {
1618 pub fn new(
1624 rows: Vec<BooleanRow>,
1625 diagram: &mut BooleanDecisionDiagram,
1626 ) -> Result<Self, BooleanResourceError> {
1627 let mut space = Self::default();
1628 for row in rows {
1629 space.push(row, diagram)?;
1630 }
1631 Ok(space)
1632 }
1633
1634 pub fn len(&self) -> usize {
1636 self.live
1637 }
1638
1639 pub fn is_empty(&self) -> bool {
1641 self.live == 0
1642 }
1643
1644 pub fn decisions_mut(&mut self) -> impl Iterator<Item = &mut DecisionId> {
1647 self.rows
1648 .iter_mut()
1649 .filter_map(Option::as_mut)
1650 .flat_map(BooleanRow::decisions_mut)
1651 }
1652
1653 pub fn rows(&self) -> impl Iterator<Item = &BooleanRow> {
1655 self.rows.iter().filter_map(Option::as_ref)
1656 }
1657
1658 pub fn indexed_rows(&self) -> impl Iterator<Item = (usize, &BooleanRow)> {
1660 self.rows
1661 .iter()
1662 .enumerate()
1663 .filter_map(|(index, row)| row.as_ref().map(|row| (index, row)))
1664 }
1665
1666 pub fn indexed_slot_count(&self) -> usize {
1668 self.rows.len()
1669 }
1670
1671 pub fn row(&self, index: usize) -> Option<&BooleanRow> {
1673 self.rows.get(index).and_then(Option::as_ref)
1674 }
1675
1676 pub fn columns(&self) -> impl ExactSizeIterator<Item = usize> + '_ {
1678 self.columns.keys().copied()
1679 }
1680
1681 pub fn rows_with_column(&self, column: usize) -> impl Iterator<Item = &BooleanRow> {
1683 self.row_ids_with_column(column)
1684 .filter_map(|index| self.row(index))
1685 }
1686
1687 pub fn row_ids_with_column(&self, column: usize) -> impl Iterator<Item = usize> + '_ {
1689 self.columns
1690 .get(&column)
1691 .into_iter()
1692 .flat_map(|rows| rows.iter())
1693 .copied()
1694 }
1695
1696 pub fn push(
1702 &mut self,
1703 row: BooleanRow,
1704 diagram: &mut BooleanDecisionDiagram,
1705 ) -> Result<(), BooleanResourceError> {
1706 self.insert_indexed(row, diagram).map(|_| ())
1707 }
1708
1709 pub fn insert_indexed(
1716 &mut self,
1717 row: BooleanRow,
1718 diagram: &mut BooleanDecisionDiagram,
1719 ) -> Result<Option<usize>, BooleanResourceError> {
1720 diagram.charge(1)?;
1721 if row.active == DECISION_FALSE {
1722 return Ok(None);
1723 }
1724 diagram.charge(row.bits.len())?;
1725 if self.rows.len() == self.rows.capacity() {
1726 diagram.charge(self.rows.len())?;
1727 }
1728 let index = self.rows.len();
1729 for &(column, _) in &row.bits {
1730 self.columns.entry(column).or_default().insert(index);
1731 }
1732 self.rows.push(Some(row));
1733 self.live += 1;
1734 Ok(Some(index))
1735 }
1736
1737 pub fn take(
1743 &mut self,
1744 index: usize,
1745 diagram: &mut BooleanDecisionDiagram,
1746 ) -> Result<Option<BooleanRow>, BooleanResourceError> {
1747 diagram.charge(1)?;
1748 let Some(row) = self.row(index) else {
1749 return Ok(None);
1750 };
1751 diagram.charge(row.bits.len())?;
1752 Ok(self.take_row(index))
1753 }
1754
1755 pub fn replace(
1765 &mut self,
1766 index: usize,
1767 row: BooleanRow,
1768 diagram: &mut BooleanDecisionDiagram,
1769 ) -> Result<Option<BooleanRow>, BooleanResourceError> {
1770 assert!(index < self.rows.len(), "row ID belongs to this space");
1771 let old_terms = self.row(index).map_or(0, |row| row.bits.len());
1772 let new_terms = if row.active == DECISION_FALSE {
1773 0
1774 } else {
1775 row.bits.len()
1776 };
1777 diagram.charge(1usize.saturating_add(old_terms).saturating_add(new_terms))?;
1778 let previous = self.take_row(index);
1779 if row.active != DECISION_FALSE {
1780 for &(column, _) in &row.bits {
1781 self.columns.entry(column).or_default().insert(index);
1782 }
1783 self.rows[index] = Some(row);
1784 self.live += 1;
1785 }
1786 Ok(previous)
1787 }
1788
1789 pub fn erase_column(
1802 &mut self,
1803 column: usize,
1804 diagram: &mut BooleanDecisionDiagram,
1805 ) -> Result<(), BooleanResourceError> {
1806 diagram.charge(1)?;
1807 let Some(indices) = self.columns.get(&column) else {
1808 return Ok(());
1809 };
1810 diagram.charge(indices.len())?;
1811 let work = indices.iter().try_fold(0usize, |work, &index| {
1812 let row = self.rows[index].as_ref().expect("indexed row exists");
1813 let search = row.bits.len().ilog2() as usize + 1;
1814 diagram.charge(search)?;
1815 let suffix =
1816 row.bits.len() - row.bits.partition_point(|&(current, _)| current < column);
1817 Ok::<_, BooleanResourceError>(work.saturating_add(search).saturating_add(suffix))
1818 })?;
1819 diagram.charge(work)?;
1822 for index in self.columns.remove(&column).expect("indexed column exists") {
1823 self.rows[index]
1824 .as_mut()
1825 .expect("indexed row exists")
1826 .set(column, DECISION_FALSE);
1827 }
1828 Ok(())
1829 }
1830
1831 pub fn into_rows(
1837 self,
1838 diagram: &mut BooleanDecisionDiagram,
1839 ) -> Result<Vec<BooleanRow>, BooleanResourceError> {
1840 diagram.charge(self.rows.len())?;
1841 Ok(self.rows.into_iter().flatten().collect())
1842 }
1843
1844 fn candidates(
1845 &self,
1846 column: usize,
1847 diagram: &mut BooleanDecisionDiagram,
1848 ) -> Result<Vec<usize>, BooleanResourceError> {
1849 diagram.charge(1)?;
1850 let Some(indices) = self.columns.get(&column) else {
1851 return Ok(Vec::new());
1852 };
1853 diagram.charge(indices.len())?;
1854 Ok(indices.iter().copied().collect())
1855 }
1856
1857 fn remove_column_row(&mut self, column: usize, index: usize) {
1858 if let std::collections::hash_map::Entry::Occupied(mut entry) = self.columns.entry(column) {
1859 entry.get_mut().remove(&index);
1860 if entry.get().is_empty() {
1861 entry.remove();
1862 }
1863 }
1864 }
1865
1866 fn take_row(&mut self, index: usize) -> Option<BooleanRow> {
1867 let row = self.rows[index].take()?;
1868 self.live -= 1;
1869 for &(column, _) in &row.bits {
1870 self.remove_column_row(column, index);
1871 }
1872 Some(row)
1873 }
1874
1875 fn xor_row(
1876 &mut self,
1877 index: usize,
1878 pivot: &BooleanRow,
1879 factor: DecisionId,
1880 diagram: &mut BooleanDecisionDiagram,
1881 ) -> Result<(), BooleanResourceError> {
1882 if factor == DECISION_FALSE {
1883 return Ok(());
1884 }
1885 diagram.charge(pivot.bits.len())?;
1888 self.rows[index]
1889 .as_mut()
1890 .expect("indexed row exists")
1891 .xor_scaled(pivot, factor, diagram)?;
1892 for &(column, _) in &pivot.bits {
1893 if self.rows[index]
1894 .as_ref()
1895 .expect("indexed row exists")
1896 .get(column)
1897 == DECISION_FALSE
1898 {
1899 self.remove_column_row(column, index);
1900 } else {
1901 self.columns.entry(column).or_default().insert(index);
1902 }
1903 }
1904 Ok(())
1905 }
1906
1907 pub fn eliminate_column(
1914 &mut self,
1915 column: usize,
1916 diagram: &mut BooleanDecisionDiagram,
1917 ) -> Result<BooleanRow, BooleanResourceError> {
1918 self.eliminate_column_when(column, DECISION_TRUE, diagram)
1919 }
1920
1921 pub fn eliminate_column_sparse(
1928 &mut self,
1929 column: usize,
1930 diagram: &mut BooleanDecisionDiagram,
1931 ) -> Result<BooleanRow, BooleanResourceError> {
1932 self.eliminate_column_when_sparse(column, DECISION_TRUE, diagram)
1933 }
1934
1935 pub fn eliminate_column_when(
1941 &mut self,
1942 column: usize,
1943 gate: DecisionId,
1944 diagram: &mut BooleanDecisionDiagram,
1945 ) -> Result<BooleanRow, BooleanResourceError> {
1946 self.eliminate_column_ordered(column, gate, false, diagram)
1947 }
1948
1949 pub fn eliminate_column_when_sparse(
1956 &mut self,
1957 column: usize,
1958 gate: DecisionId,
1959 diagram: &mut BooleanDecisionDiagram,
1960 ) -> Result<BooleanRow, BooleanResourceError> {
1961 self.eliminate_column_ordered(column, gate, true, diagram)
1962 }
1963
1964 fn eliminate_column_ordered(
1965 &mut self,
1966 column: usize,
1967 gate: DecisionId,
1968 sparse: bool,
1969 diagram: &mut BooleanDecisionDiagram,
1970 ) -> Result<BooleanRow, BooleanResourceError> {
1971 if gate == DECISION_FALSE {
1972 return Ok(BooleanRow::new(DECISION_FALSE));
1973 }
1974 let mut candidates = self.candidates(column, diagram)?;
1975 if sparse {
1976 self.sort_sparse_candidates(&mut candidates, diagram)?;
1977 }
1978 let pivot = self.eliminate_candidates(candidates, diagram, |row, diagram| {
1979 diagram.apply(BooleanOp::And, gate, row.get(column))
1980 })?;
1981 if gate == DECISION_TRUE {
1984 self.columns.remove(&column);
1985 }
1986 Ok(pivot)
1987 }
1988
1989 pub fn eliminate_linear(
1996 &mut self,
1997 terms: &[(usize, DecisionId)],
1998 diagram: &mut BooleanDecisionDiagram,
1999 ) -> Result<BooleanRow, BooleanResourceError> {
2000 diagram.charge(terms.len())?;
2001 let mut candidates = BTreeSet::new();
2002 for &(column, weight) in terms {
2003 if weight != DECISION_FALSE
2004 && let Some(indices) = self.columns.get(&column)
2005 {
2006 diagram.charge(indices.len())?;
2007 candidates.extend(indices.iter().copied());
2008 }
2009 }
2010 diagram.charge(candidates.len())?;
2011 let candidates = candidates.into_iter().collect::<Vec<_>>();
2012 self.eliminate_candidates(candidates, diagram, |row, diagram| {
2013 diagram.charge(terms.len())?;
2014 terms
2015 .iter()
2016 .try_fold(DECISION_FALSE, |sum, &(column, weight)| {
2017 let term = diagram.apply(BooleanOp::And, weight, row.get(column))?;
2018 diagram.apply(BooleanOp::Xor, sum, term)
2019 })
2020 })
2021 }
2022
2023 fn sort_sparse_candidates(
2024 &self,
2025 candidates: &mut [usize],
2026 diagram: &mut BooleanDecisionDiagram,
2027 ) -> Result<(), BooleanResourceError> {
2028 if candidates.len() < 2 {
2029 return Ok(());
2030 }
2031 diagram.charge(
2033 candidates
2034 .len()
2035 .saturating_mul(candidates.len().ilog2() as usize + 1),
2036 )?;
2037 candidates.sort_unstable_by_key(|&index| {
2038 (
2039 self.rows[index]
2040 .as_ref()
2041 .expect("indexed row exists")
2042 .bits
2043 .len(),
2044 index,
2045 )
2046 });
2047 Ok(())
2048 }
2049
2050 fn eliminate_candidates(
2051 &mut self,
2052 candidates: Vec<usize>,
2053 diagram: &mut BooleanDecisionDiagram,
2054 mut coefficient: impl FnMut(
2055 &BooleanRow,
2056 &mut BooleanDecisionDiagram,
2057 ) -> Result<DecisionId, BooleanResourceError>,
2058 ) -> Result<BooleanRow, BooleanResourceError> {
2059 let mut pivot = BooleanRow::new(DECISION_FALSE);
2060 for &index in &candidates {
2061 diagram.charge(1)?;
2062 let row = self.rows[index].as_mut().expect("indexed row exists");
2063 let coefficient = coefficient(row, diagram)?;
2064 select_into_pivot(&mut pivot, row, coefficient, diagram)?;
2065 if row.active == DECISION_FALSE {
2066 self.take(index, diagram)?;
2067 }
2068 }
2069 for index in candidates {
2070 diagram.charge(1)?;
2071 let Some(row) = &self.rows[index] else {
2072 continue;
2073 };
2074 let coefficient = coefficient(row, diagram)?;
2075 let factor = diagram.apply(BooleanOp::And, row.active, coefficient)?;
2076 self.xor_row(index, &pivot, factor, diagram)?;
2077 }
2078 Ok(pivot)
2079 }
2080
2081 pub fn reduce_column(
2092 &mut self,
2093 column: usize,
2094 pivot: &BooleanRow,
2095 diagram: &mut BooleanDecisionDiagram,
2096 ) -> Result<(), BooleanResourceError> {
2097 if pivot.active == DECISION_FALSE {
2098 return Ok(());
2099 }
2100 for index in self.candidates(column, diagram)? {
2101 diagram.charge(1)?;
2102 let row = self.rows[index].as_ref().expect("indexed row exists");
2103 let factor = diagram.apply(BooleanOp::And, row.active, row.get(column))?;
2104 self.xor_row(index, pivot, factor, diagram)?;
2105 }
2106 Ok(())
2107 }
2108
2109 pub fn reduce_linear(
2118 &mut self,
2119 terms: &[(usize, DecisionId)],
2120 pivot: &BooleanRow,
2121 diagram: &mut BooleanDecisionDiagram,
2122 ) -> Result<(), BooleanResourceError> {
2123 if pivot.active == DECISION_FALSE {
2124 return Ok(());
2125 }
2126 diagram.charge(terms.len())?;
2127 let mut candidates = BTreeSet::new();
2128 for &(column, weight) in terms {
2129 if weight != DECISION_FALSE {
2130 candidates.extend(self.candidates(column, diagram)?);
2131 }
2132 }
2133 for index in candidates {
2134 let row = self.rows[index].as_ref().expect("indexed row exists");
2135 diagram.charge(terms.len())?;
2136 let coefficient = terms
2137 .iter()
2138 .try_fold(DECISION_FALSE, |sum, &(column, weight)| {
2139 let term = diagram.apply(BooleanOp::And, weight, row.get(column))?;
2140 diagram.apply(BooleanOp::Xor, sum, term)
2141 })?;
2142 let factor = diagram.apply(BooleanOp::And, row.active, coefficient)?;
2143 self.xor_row(index, pivot, factor, diagram)?;
2144 }
2145 Ok(())
2146 }
2147}
2148
2149fn select_into_pivot(
2157 pivot: &mut BooleanRow,
2158 row: &mut BooleanRow,
2159 coefficient: DecisionId,
2160 diagram: &mut BooleanDecisionDiagram,
2161) -> Result<(), BooleanResourceError> {
2162 let candidate = diagram.apply(BooleanOp::And, row.active, coefficient)?;
2163 let absent = diagram.negate(pivot.active)?;
2164 let selected = diagram.apply(BooleanOp::And, candidate, absent)?;
2165 pivot.xor_scaled(row, selected, diagram)?;
2166 pivot.active = diagram.apply(BooleanOp::Or, pivot.active, candidate)?;
2167 let remaining = diagram.negate(selected)?;
2168 row.active = diagram.apply(BooleanOp::And, row.active, remaining)?;
2169 Ok(())
2170}
2171
2172pub fn eliminate_boolean_rows(
2181 rows: &mut Vec<BooleanRow>,
2182 diagram: &mut BooleanDecisionDiagram,
2183 mut constraint: impl FnMut(
2184 &BooleanRow,
2185 &mut BooleanDecisionDiagram,
2186 ) -> Result<DecisionId, BooleanResourceError>,
2187) -> Result<BooleanRow, BooleanResourceError> {
2188 diagram.charge(rows.len().saturating_mul(2))?;
2189 let mut pivot = BooleanRow::new(DECISION_FALSE);
2190 let mut factors = Vec::with_capacity(rows.len());
2191 for row in rows.iter_mut() {
2192 let coefficient = constraint(row, diagram)?;
2193 factors.push(coefficient);
2194 select_into_pivot(&mut pivot, row, coefficient, diagram)?;
2195 }
2196 for (row, coefficient) in rows.iter_mut().zip(factors) {
2197 let factor = diagram.apply(BooleanOp::And, row.active, coefficient)?;
2198 row.xor_scaled(&pivot, factor, diagram)?;
2199 }
2200 rows.retain(|row| row.active != DECISION_FALSE);
2201 Ok(pivot)
2202}
2203
2204pub fn reduce_boolean_rows(
2210 rows: Vec<BooleanRow>,
2211 columns: impl IntoIterator<Item = usize>,
2212 diagram: &mut BooleanDecisionDiagram,
2213) -> Result<Vec<BooleanRow>, BooleanResourceError> {
2214 let mut remaining = BooleanRowSpace::new(rows, diagram)?;
2215 let mut reduced = BooleanRowSpace::default();
2216 for column in columns {
2217 if remaining.is_empty() || remaining.columns.is_empty() {
2218 break;
2219 }
2220 let pivot = remaining.eliminate_column(column, diagram)?;
2221 if pivot.active != DECISION_FALSE {
2222 reduced.reduce_column(column, &pivot, diagram)?;
2223 reduced.push(pivot, diagram)?;
2224 }
2225 }
2226 reduced.into_rows(diagram)
2227}
2228
2229#[cfg(test)]
2230mod tests {
2231 use super::*;
2232
2233 #[test]
2234 fn linear_reduction_matches_an_explicit_guarded_query_coordinate() {
2235 let mut diagram = BooleanDecisionDiagram::default();
2236 let selector = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
2237 let other = diagram.negate(selector).unwrap();
2238 let terms = [(0, selector), (1, other), (2, selector), (2, selector)];
2239 let pivot = BooleanRow::from_terms(
2240 DECISION_TRUE,
2241 [(0, DECISION_TRUE), (1, DECISION_TRUE), (5, selector)],
2242 );
2243 let rows = vec![
2244 BooleanRow::from_terms(
2245 DECISION_TRUE,
2246 [
2247 (0, other),
2248 (1, selector),
2249 (2, DECISION_TRUE),
2250 (6, DECISION_TRUE),
2251 ],
2252 ),
2253 BooleanRow::from_terms(
2254 selector,
2255 [(0, DECISION_TRUE), (2, DECISION_TRUE), (7, DECISION_TRUE)],
2256 ),
2257 ];
2258 let mut expected = rows.clone();
2259 for (row, query) in expected.iter_mut().zip([DECISION_FALSE, selector]) {
2262 row.set(10, query);
2263 }
2264 let mut tagged = BooleanRowSpace::new(expected, &mut diagram).unwrap();
2265 let mut tagged_pivot = pivot.clone();
2266 tagged_pivot.set(10, DECISION_TRUE);
2267 tagged
2268 .reduce_column(10, &tagged_pivot, &mut diagram)
2269 .unwrap();
2270 let mut compact = BooleanRowSpace::new(rows, &mut diagram).unwrap();
2271 compact.reduce_linear(&terms, &pivot, &mut diagram).unwrap();
2272 let mut expected = tagged.into_rows(&mut diagram).unwrap();
2273 for row in &mut expected {
2274 row.set(10, DECISION_FALSE);
2275 }
2276 let compact = compact.into_rows(&mut diagram).unwrap();
2277 assert_eq!(compact.len(), expected.len());
2278 for (actual, expected) in compact.iter().zip(expected) {
2279 assert_eq!(actual.active, expected.active);
2280 assert_eq!(actual.terms(), expected.terms());
2281 }
2282 }
2283
2284 #[test]
2285 fn deferred_witness_failures_leave_rows_unchanged() {
2286 for exhaust_core_work in [false, true] {
2287 let mut diagram = BooleanDecisionDiagram::default();
2288 diagram
2289 .start_witness_tape([0], if exhaust_core_work { 16 } else { 2 })
2290 .unwrap();
2291 let mut left =
2292 BooleanRow::from_terms(DECISION_TRUE, [(0, DECISION_TRUE), (10, DECISION_TRUE)]);
2293 let mut right =
2294 BooleanRow::from_terms(DECISION_TRUE, [(0, DECISION_TRUE), (11, DECISION_TRUE)]);
2295 left.bits.reserve(1_024);
2296 left.defer_coefficients(&mut diagram).unwrap();
2297 right.defer_coefficients(&mut diagram).unwrap();
2298 assert!(
2299 left.bits.capacity() < 1_024,
2300 "split released passive capacity"
2301 );
2302 let before = left.clone();
2303 if exhaust_core_work {
2304 diagram.limits.max_steps = diagram.steps() + 2;
2306 }
2307 let error = left
2308 .xor_scaled(&right, DECISION_TRUE, &mut diagram)
2309 .unwrap_err();
2310 assert_eq!(
2311 error.resource,
2312 if exhaust_core_work {
2313 "Boolean work steps"
2314 } else {
2315 "row witness operations"
2316 },
2317 );
2318 assert_eq!(left.active, before.active);
2319 assert_eq!(left.bits, before.bits);
2320 assert_eq!(left.witness, before.witness);
2321 }
2322 let mut diagram = BooleanDecisionDiagram::default();
2323 diagram.start_witness_tape([0], 8).unwrap();
2324 let mut core_only = BooleanRow::from_terms(DECISION_TRUE, [(0, DECISION_TRUE)]);
2325 core_only.defer_coefficients(&mut diagram).unwrap();
2326 assert_eq!(core_only.witness, RowWitness::Deferred(None));
2327 let untracked = BooleanRow::from_terms(DECISION_TRUE, [(10, DECISION_TRUE)]);
2328 assert!(
2329 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2330 core_only
2331 .xor_scaled(&untracked, DECISION_TRUE, &mut diagram)
2332 .unwrap();
2333 }))
2334 .is_err()
2335 );
2336 std::panic::catch_unwind(|| core_only.into_terms()).unwrap_err();
2337 }
2338
2339 #[test]
2340 fn deferred_witnesses_preserve_raw_payload_through_pivots_and_collection() {
2341 let mut original = BooleanDecisionDiagram::default();
2342 original
2343 .make_node(99, DECISION_FALSE, DECISION_TRUE)
2344 .unwrap();
2345 let a = original
2346 .make_node(0, DECISION_FALSE, DECISION_TRUE)
2347 .unwrap();
2348 let b = original
2349 .make_node(1, DECISION_FALSE, DECISION_TRUE)
2350 .unwrap();
2351 let absent = original.negate(a).unwrap();
2352 let rows = vec![
2353 BooleanRow::from_terms(DECISION_TRUE, [(0, a), (1, b), (100, DECISION_TRUE)]),
2354 BooleanRow::from_terms(a, [(0, DECISION_TRUE), (1, a), (101, b)]),
2355 BooleanRow::from_terms(absent, [(0, DECISION_TRUE), (1, b), (102, a)]),
2356 ];
2357 let mut expanded_diagram = original.clone();
2358 let mut expanded = BooleanRowSpace::new(rows.clone(), &mut expanded_diagram).unwrap();
2359 let mut tape_diagram = original;
2360 tape_diagram.start_witness_tape([0, 1], 100).unwrap();
2361 let mut core = rows;
2362 for row in &mut core {
2363 row.defer_coefficients(&mut tape_diagram).unwrap();
2364 }
2365 let mut core = BooleanRowSpace::new(core, &mut tape_diagram).unwrap();
2366 let mut expected = Vec::new();
2367 let mut actual = Vec::new();
2368 for column in [0, 1] {
2369 expected.push(
2370 expanded
2371 .eliminate_column(column, &mut expanded_diagram)
2372 .unwrap(),
2373 );
2374 actual.push(core.eliminate_column(column, &mut tape_diagram).unwrap());
2375 }
2376 expected.extend(expanded.into_rows(&mut expanded_diagram).unwrap());
2377 actual.extend(core.into_rows(&mut tape_diagram).unwrap());
2378 assert!(tape_diagram.witness_collection_work() > 0);
2379 assert!(
2380 tape_diagram.collect_garbage(actual.iter_mut().flat_map(BooleanRow::decisions_mut)) > 0
2381 );
2382 expanded_diagram.collect_garbage(expected.iter_mut().flat_map(BooleanRow::decisions_mut));
2383 let columns = [102, 0, 101, 1, 100, 101, 99, usize::MAX];
2384 let mut probes = actual
2385 .iter()
2386 .map(|row| row.probe_coefficients(&columns, &mut tape_diagram).unwrap())
2387 .collect::<Vec<_>>();
2388 tape_diagram.collect_garbage(
2389 actual
2390 .iter_mut()
2391 .flat_map(BooleanRow::decisions_mut)
2392 .chain(probes.iter_mut().flatten()),
2393 );
2394 for (row, probe) in actual.iter_mut().zip(probes) {
2395 row.expand_coefficients(&mut tape_diagram).unwrap();
2396 assert_eq!(probe, columns.map(|column| row.get(column)));
2397 }
2398 tape_diagram.finish_witness_tape();
2399 let mut remap = vec![DECISION_FALSE, DECISION_TRUE];
2400 for node in tape_diagram.nodes() {
2401 remap.push(
2402 expanded_diagram
2403 .make_node(node.variable, remap[node.low.0], remap[node.high.0])
2404 .unwrap(),
2405 );
2406 }
2407 assert_eq!(actual.len(), expected.len());
2408 for (actual, expected) in actual.iter().zip(expected) {
2409 assert_eq!(remap[actual.active.0], expected.active);
2410 assert_eq!(
2411 actual
2412 .terms()
2413 .iter()
2414 .map(|&(col, value)| (col, remap[value.0]))
2415 .collect::<Vec<_>>(),
2416 expected.terms()
2417 );
2418 }
2419 let mut limited = BooleanDecisionDiagram::default();
2420 limited.start_witness_tape([], 0).unwrap();
2421 assert!(matches!(
2422 BooleanRow::from_terms(DECISION_TRUE, [(10, DECISION_TRUE)])
2423 .defer_coefficients(&mut limited),
2424 Err(BooleanResourceError {
2425 resource: "row witness operations",
2426 observed: 1,
2427 limit: 0
2428 })
2429 ));
2430 }
2431
2432 #[test]
2433 fn composed_queries_cancel_shared_paths_and_keep_raw_inactive_values() {
2434 let mut diagram = BooleanDecisionDiagram::default();
2435 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
2436 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
2437 diagram.start_witness_tape([0], 20).unwrap();
2438 let mut leaf = BooleanRow::from_terms(a, [(0, b), (20, DECISION_TRUE), (22, a)]);
2439 leaf.defer_coefficients(&mut diagram).unwrap();
2440 let mut combined = BooleanRow::new(DECISION_FALSE);
2441 combined.xor_scaled(&leaf, a, &mut diagram).unwrap();
2442 combined.xor_scaled(&leaf, b, &mut diagram).unwrap();
2443 let columns = [22, 20, 0, 21, 20, usize::MAX];
2444 let probe = combined.probe_coefficients(&columns, &mut diagram).unwrap();
2445 let mut eager = combined.clone();
2446 eager.expand_coefficients(&mut diagram).unwrap();
2447 assert_eq!(probe, columns.map(|column| eager.get(column)));
2448 assert_eq!(combined.active, DECISION_FALSE);
2449 assert_ne!(
2450 probe[1], DECISION_FALSE,
2451 "raw query must not apply activation"
2452 );
2453 assert_eq!(probe[3], DECISION_FALSE, "interval holes remain zero");
2454 assert!(matches!(combined.witness, RowWitness::Deferred(Some(_))));
2455 let factor = diagram.apply(BooleanOp::Xor, a, b).unwrap();
2456 combined.xor_scaled(&leaf, factor, &mut diagram).unwrap();
2457 assert_eq!(
2458 combined.probe_coefficients(&columns, &mut diagram).unwrap(),
2459 vec![DECISION_FALSE; columns.len()],
2460 );
2461 }
2462
2463 #[test]
2464 fn composed_queries_prune_disjoint_history_and_restore_tape_on_exhaustion() {
2465 let mut diagram = BooleanDecisionDiagram::default();
2466 diagram.start_witness_tape([], 4096).unwrap();
2467 let mut row = BooleanRow::default();
2468 let mut costs = Vec::new();
2469 for column in 0..1024 {
2470 let mut leaf = BooleanRow::from_terms(DECISION_TRUE, [(column, DECISION_TRUE)]);
2471 leaf.defer_coefficients(&mut diagram).unwrap();
2472 row.xor_scaled(&leaf, DECISION_TRUE, &mut diagram).unwrap();
2473 if [63, 127, 255, 511, 1023].contains(&column) {
2474 let start = diagram.steps();
2475 assert_eq!(
2476 row.probe_coefficients(&[column], &mut diagram).unwrap(),
2477 [DECISION_TRUE],
2478 );
2479 costs.push(diagram.steps() - start);
2480 }
2481 }
2482 assert!(costs.windows(2).all(|pair| pair[0] == pair[1]), "{costs:?}");
2483 assert!(row.terms().is_empty(), "querying never expands the row");
2484 let before = row.clone();
2485 let limit = diagram.steps() + 7;
2486 diagram.limits.max_steps = limit;
2487 let error = row.probe_coefficients(&[1023], &mut diagram).unwrap_err();
2488 assert_eq!(error.resource, "Boolean work steps");
2489 assert_eq!(error.limit, limit);
2490 assert_eq!(row.witness, before.witness);
2491 assert_eq!(row.terms(), before.terms());
2492 assert!(diagram.row_witnesses.is_some());
2493 let spent = diagram.steps();
2494 diagram.collect_garbage(row.decisions_mut());
2495 assert_eq!(diagram.steps(), spent);
2496 diagram.limits.max_steps = usize::MAX;
2497 assert_eq!(
2498 row.probe_coefficients(&[1023], &mut diagram).unwrap(),
2499 [DECISION_TRUE],
2500 );
2501 }
2502
2503 #[test]
2504 fn batched_witness_fronts_share_overlapping_history() {
2505 for width in [64, 128, 256, 512, 1024] {
2506 let mut diagram = BooleanDecisionDiagram::default();
2507 diagram.start_witness_tape([], 2 * width).unwrap();
2508 let mut prefix = BooleanRow::default();
2509 let mut rows = Vec::new();
2510 for column in 0..width {
2511 let mut leaf = BooleanRow::from_terms(DECISION_TRUE, [(column, DECISION_TRUE)]);
2512 leaf.defer_coefficients(&mut diagram).unwrap();
2513 prefix
2514 .xor_scaled(&leaf, DECISION_TRUE, &mut diagram)
2515 .unwrap();
2516 rows.push(prefix.clone());
2517 }
2518 let before = diagram.steps();
2519 diagram.extend_witness_front(&[0], &mut rows).unwrap();
2520 let work = diagram.steps() - before;
2521 assert!(
2522 work < 32 * width,
2523 "{width} overlapping roots spent {work} steps"
2524 );
2525 assert!(rows.iter().all(|row| row.terms() == [(0, DECISION_TRUE)]));
2526 let last = rows.last_mut().unwrap();
2527 last.expand_coefficients(&mut diagram).unwrap();
2528 assert_eq!(last.terms().len(), width);
2529 }
2530 }
2531
2532 #[test]
2533 fn batched_witness_fronts_do_not_cross_independent_rows() {
2534 let mut previous = None;
2535 for width in [64, 128, 256, 512, 1024] {
2536 let mut diagram = BooleanDecisionDiagram::default();
2537 diagram.start_witness_tape([], 3 * width).unwrap();
2538 let mut rows = Vec::new();
2539 for index in 0..width {
2540 let mut left = BooleanRow::from_terms(DECISION_TRUE, [(2 * index, DECISION_TRUE)]);
2541 let mut right =
2542 BooleanRow::from_terms(DECISION_TRUE, [(2 * index + 1, DECISION_TRUE)]);
2543 left.defer_coefficients(&mut diagram).unwrap();
2544 right.defer_coefficients(&mut diagram).unwrap();
2545 left.xor_scaled(&right, DECISION_TRUE, &mut diagram)
2546 .unwrap();
2547 rows.push(left);
2548 }
2549 let columns = (0..width).map(|index| 2 * index).collect::<Vec<_>>();
2550 let before = diagram.steps();
2551 diagram.extend_witness_front(&columns, &mut rows).unwrap();
2552 let work = diagram.steps() - before;
2553 if let Some(previous) = previous {
2554 assert!(
2555 work < 3 * previous,
2556 "{width}: projection became a dense cross product"
2557 );
2558 }
2559 previous = Some(work);
2560 for (index, row) in rows.iter().enumerate() {
2561 assert_eq!(row.terms(), [(2 * index, DECISION_TRUE)]);
2562 assert_eq!(row.bits.capacity(), 1);
2563 }
2564 }
2565 }
2566
2567 #[test]
2568 fn promoted_witness_fronts_preserve_raw_rows_through_algebra_and_collection() {
2569 let mut diagram = BooleanDecisionDiagram::default();
2570 diagram
2571 .make_node(99, DECISION_FALSE, DECISION_TRUE)
2572 .unwrap();
2573 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
2574 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
2575 let mut expected = vec![
2576 BooleanRow::from_terms(DECISION_FALSE, [(0, a), (20, b), (22, DECISION_TRUE)]),
2577 BooleanRow::from_terms(DECISION_TRUE, [(0, b), (21, a), (31, b)]),
2578 BooleanRow::from_terms(a, [(20, DECISION_TRUE), (21, b), (22, a)]),
2579 ];
2580 let mut actual = expected.clone();
2581 diagram.start_witness_tape([0], 100).unwrap();
2582 for row in &mut actual {
2583 row.defer_coefficients(&mut diagram).unwrap();
2584 }
2585 let handles = actual.iter().map(|row| row.witness).collect::<Vec<_>>();
2586 diagram
2587 .extend_witness_front(&[21, 20, 20, 0, 99], &mut actual)
2588 .unwrap();
2589 for (row, handle) in actual.iter().zip(handles) {
2590 assert_eq!(row.witness, handle);
2591 assert_eq!(
2592 row.bits.capacity(),
2593 row.bits.len(),
2594 "absent columns reserve no slots"
2595 );
2596 }
2597 assert_eq!(actual[0].active, DECISION_FALSE);
2598 assert_eq!(
2599 actual[0].get(20),
2600 b,
2601 "promotion does not mask inactive rows"
2602 );
2603 assert_eq!(
2604 actual[0].get(22),
2605 DECISION_FALSE,
2606 "unqueried support stays passive"
2607 );
2608 let columns = [31, 22, 20, 0, 21, 20, 99];
2609 for (row, eager) in actual.iter().zip(&expected) {
2610 assert_eq!(
2611 row.probe_coefficients(&columns, &mut diagram).unwrap(),
2612 columns.map(|column| eager.get(column)),
2613 );
2614 }
2615 for rows in [&mut actual, &mut expected] {
2616 let (left, right) = rows.split_at_mut(1);
2617 left[0].xor_scaled(&right[1], b, &mut diagram).unwrap();
2618 let source = right[1].clone();
2619 right[0].xor_scaled(&source, a, &mut diagram).unwrap();
2620 }
2621 diagram
2622 .extend_witness_front(&[31, 20], &mut actual)
2623 .unwrap();
2624 let fresh = BooleanRow::from_terms(DECISION_TRUE, [(20, a), (50, b)]);
2625 expected.push(fresh.clone());
2626 let mut fresh = fresh;
2627 fresh.defer_coefficients(&mut diagram).unwrap();
2628 assert_eq!(fresh.get(20), a, "new leaves use the extended front");
2629 actual.push(fresh);
2630
2631 let pivot = |rows, diagram: &mut BooleanDecisionDiagram| {
2632 let mut space = BooleanRowSpace::new(rows, diagram).unwrap();
2633 let first = space
2634 .eliminate_linear(&[(20, b), (21, DECISION_TRUE)], diagram)
2635 .unwrap();
2636 let second = space.eliminate_column(31, diagram).unwrap();
2637 let mut rows = vec![first, second];
2638 rows.extend(space.into_rows(diagram).unwrap());
2639 rows
2640 };
2641 let mut actual = pivot(actual, &mut diagram);
2642 let mut expected = pivot(expected, &mut diagram);
2643 assert!(
2644 diagram.collect_garbage(
2645 actual
2646 .iter_mut()
2647 .chain(&mut expected)
2648 .flat_map(BooleanRow::decisions_mut)
2649 ) > 0
2650 );
2651 for (row, eager) in actual.iter_mut().zip(&expected) {
2652 row.expand_coefficients(&mut diagram).unwrap();
2653 assert_eq!(row.active, eager.active);
2654 assert_eq!(row.terms(), eager.terms());
2655 }
2656 assert_eq!(actual.len(), expected.len());
2657 }
2658
2659 #[test]
2660 fn witness_front_promotion_failures_retain_partition_rows_and_spent_work() {
2661 let mut diagram = BooleanDecisionDiagram::default();
2662 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
2663 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
2664 diagram.start_witness_tape([0], 20).unwrap();
2665 let mut leaf = BooleanRow::from_terms(DECISION_TRUE, [(20, b), (30, DECISION_TRUE)]);
2666 leaf.defer_coefficients(&mut diagram).unwrap();
2667 let mut combined = BooleanRow::new(DECISION_FALSE);
2668 combined.xor_scaled(&leaf, a, &mut diagram).unwrap();
2669 let rows = vec![leaf, combined, BooleanRow::from_terms(a, [(20, b)])];
2670 let columns = [30, 20, 30];
2671 let before_steps = diagram.steps();
2672 let mut completed = diagram.clone();
2673 completed
2674 .extend_witness_front(&columns, &mut rows.clone())
2675 .unwrap();
2676 let work = completed.steps() - before_steps;
2677
2678 for allowance in 0..=work {
2680 let mut limited = diagram.clone();
2681 let mut pending = rows.clone();
2682 if allowance == work {
2683 limited.limits.max_nodes = limited.nodes().len();
2684 } else {
2685 limited.limits.max_steps = before_steps + allowance;
2686 }
2687 let error = limited
2688 .extend_witness_front(&columns, &mut pending)
2689 .unwrap_err();
2690 assert_eq!(
2691 error.resource,
2692 if allowance == work {
2693 "Boolean nodes"
2694 } else {
2695 "Boolean work steps"
2696 }
2697 );
2698 for (actual, expected) in pending.iter().zip(&rows) {
2699 assert_eq!(actual.active, expected.active);
2700 assert_eq!(actual.witness, expected.witness);
2701 assert_eq!(actual.terms(), expected.terms());
2702 }
2703 let tape = limited.row_witnesses.as_ref().unwrap();
2704 assert_eq!(tape.explicit_columns, [0].into_iter().collect());
2705 assert_eq!(tape.initial_explicit_columns, 1);
2706 assert_eq!(
2707 tape.ops.len(),
2708 diagram.row_witnesses.as_ref().unwrap().ops.len()
2709 );
2710 assert!(limited.steps() >= before_steps);
2711 let spent = limited.steps();
2712 limited.collect_garbage(pending.iter_mut().flat_map(BooleanRow::decisions_mut));
2713 assert_eq!(limited.steps(), spent);
2714 limited.limits = BooleanLimits::UNLIMITED;
2715 limited
2716 .extend_witness_front(&columns, &mut pending)
2717 .unwrap();
2718 for row in &mut pending {
2719 row.expand_coefficients(&mut limited).unwrap();
2720 }
2721 assert!(
2722 pending
2723 .iter()
2724 .all(|row| row.witness == RowWitness::Explicit)
2725 );
2726 }
2727 }
2728
2729 #[test]
2730 fn limits_stop_allocations_and_survive_collection_and_cloning() {
2731 let mut diagram = BooleanDecisionDiagram::with_limits(BooleanLimits {
2732 max_nodes: 1,
2733 max_steps: 2,
2734 });
2735 let mut root = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
2736 assert_eq!(
2737 diagram.make_node(0, DECISION_FALSE, DECISION_TRUE),
2738 Ok(root)
2739 );
2740 let error = diagram
2741 .make_node(1, DECISION_FALSE, DECISION_TRUE)
2742 .unwrap_err();
2743 assert_eq!(
2744 error,
2745 BooleanResourceError {
2746 resource: "Boolean nodes",
2747 observed: 2,
2748 limit: 1,
2749 }
2750 );
2751 assert_eq!(diagram.nodes().len(), 1);
2752 let limits = diagram.limits();
2753 diagram.collect_garbage([&mut root]);
2754 diagram.collect_garbage(std::iter::empty());
2755 assert_eq!(diagram.limits(), limits);
2756 assert_eq!(diagram.steps(), 1);
2757 let mut cloned = diagram.clone();
2758 cloned.charge(1).unwrap();
2759 let error = cloned
2760 .make_node(1, DECISION_FALSE, DECISION_TRUE)
2761 .unwrap_err();
2762 assert_eq!(
2763 error,
2764 BooleanResourceError {
2765 resource: "Boolean work steps",
2766 observed: 3,
2767 limit: 2,
2768 }
2769 );
2770 assert!(cloned.nodes().is_empty());
2771 assert_eq!(cloned.steps(), 2);
2772
2773 let mut diagram = BooleanDecisionDiagram::with_limits(BooleanLimits {
2774 max_nodes: 10,
2775 max_steps: 4,
2776 });
2777 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
2778 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
2779 diagram.apply(BooleanOp::And, a, b).unwrap_err();
2780 assert_eq!(diagram.steps(), 4);
2781 assert_eq!(diagram.nodes().len(), 2);
2782 diagram.negate(a).unwrap_err();
2783 diagram.constrain(a, b).unwrap_err();
2784 assert_eq!(diagram.nodes().len(), 2);
2785 }
2786
2787 #[test]
2788 fn collection_keeps_only_live_cached_complements() {
2789 let mut diagram = BooleanDecisionDiagram::default();
2790 let _garbage = diagram.make_node(8, DECISION_FALSE, DECISION_TRUE).unwrap();
2791 let mut value = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
2792 let mut complement = diagram.negate(value).unwrap();
2793 assert_eq!(
2794 diagram.cached_complement(DECISION_FALSE),
2795 Some(DECISION_TRUE)
2796 );
2797 assert_eq!(
2798 diagram.cached_complement(DECISION_TRUE),
2799 Some(DECISION_FALSE)
2800 );
2801
2802 assert_eq!(diagram.collect_garbage([&mut value, &mut complement]), 1);
2803 assert_eq!(diagram.cached_complement(value), Some(complement));
2804 assert_eq!(diagram.cached_complement(complement), Some(value));
2805 assert_eq!(diagram.collect_garbage([&mut value, &mut complement]), 0);
2806 assert_eq!(diagram.cached_complement(value), Some(complement));
2807
2808 assert_eq!(diagram.collect_garbage([&mut value]), 1);
2809 assert_eq!(diagram.cached_complement(value), None);
2810 assert_eq!(diagram.collect_garbage(std::iter::empty()), 1);
2811 assert_eq!(
2812 diagram.cached_complement(DECISION_FALSE),
2813 Some(DECISION_TRUE)
2814 );
2815 assert_eq!(
2816 diagram.cached_complement(DECISION_TRUE),
2817 Some(DECISION_FALSE)
2818 );
2819 }
2820
2821 #[test]
2822 fn constant_rows_and_queries_spend_work_before_materializing() {
2823 let mut diagram = BooleanDecisionDiagram::with_limits(BooleanLimits {
2824 max_nodes: 0,
2825 max_steps: 3,
2826 });
2827 let other = BooleanRow::from_terms(DECISION_TRUE, (0..4).map(|i| (i, DECISION_TRUE)));
2828 let mut row = BooleanRow::default();
2829 assert!(row.xor_scaled(&other, DECISION_TRUE, &mut diagram).is_err());
2830 assert!(row.terms().is_empty());
2831 assert!(diagram.nodes().is_empty());
2832 diagram.values_up_to(&[DECISION_TRUE; 4], 1).unwrap_err();
2833 diagram
2834 .conjunction_witness(&[DECISION_TRUE; 4])
2835 .unwrap_err();
2836 assert_eq!(diagram.steps(), 0);
2837 let reduced = reduce_boolean_rows(vec![other], 0..4, &mut diagram);
2838 assert!(reduced.is_err(), "constant pivots still consume work");
2839 assert!(diagram.nodes().is_empty());
2840 }
2841
2842 #[test]
2843 fn iterative_operations_preserve_exact_functions_on_deep_chains() {
2844 const DEPTH: usize = 20_000;
2845 let mut diagram = BooleanDecisionDiagram::default();
2846 let a = diagram
2847 .make_node(DEPTH, DECISION_FALSE, DECISION_TRUE)
2848 .unwrap();
2849 let b = diagram
2850 .make_node(DEPTH + 1, DECISION_FALSE, DECISION_TRUE)
2851 .unwrap();
2852 let (mut left, mut right) = (a, b);
2853 for variable in (0..DEPTH).rev() {
2854 left = diagram.make_node(variable, DECISION_FALSE, left).unwrap();
2855 right = diagram.make_node(variable, DECISION_FALSE, right).unwrap();
2856 }
2857 let negated = diagram.negate(right).unwrap();
2858 let joined = diagram.apply(BooleanOp::And, left, negated).unwrap();
2859 assert!(diagram.evaluate(joined, |variable| variable != DEPTH + 1));
2860 assert!(!diagram.evaluate(joined, |_| true));
2861 assert_eq!(diagram.constrain(left, right).unwrap(), a);
2862 let witness = diagram
2863 .conjunction_witness(&[left, negated])
2864 .unwrap()
2865 .unwrap();
2866 assert_eq!(witness.len(), DEPTH + 2);
2867 assert!(!witness[&(DEPTH + 1)]);
2868 assert_eq!(
2869 diagram.conjunction_witness(&[right, negated]).unwrap(),
2870 None
2871 );
2872 assert_eq!(
2873 diagram.values_up_to(&[left, right], 5).unwrap(),
2874 vec![
2875 vec![false, false],
2876 vec![false, true],
2877 vec![true, false],
2878 vec![true, true],
2879 ]
2880 );
2881 }
2882
2883 #[test]
2884 fn bounded_queries_and_operations_match_truth_tables() {
2885 let mut diagram = BooleanDecisionDiagram::default();
2886 let variables = (0..3)
2887 .map(|i| diagram.make_node(i, DECISION_FALSE, DECISION_TRUE).unwrap())
2888 .collect::<Vec<_>>();
2889 let mut roots = vec![DECISION_FALSE, DECISION_TRUE];
2890 roots.extend(&variables);
2891 for operator in [BooleanOp::Xor, BooleanOp::And, BooleanOp::Or] {
2892 roots.push(diagram.apply(operator, variables[0], variables[1]).unwrap());
2893 }
2894 roots.push(diagram.negate(variables[2]).unwrap());
2895 for &left in &roots {
2896 for &right in &roots {
2897 let operations = [BooleanOp::Xor, BooleanOp::And, BooleanOp::Or]
2898 .map(|operator| diagram.apply(operator, left, right).unwrap());
2899 let constrained = diagram.constrain(left, right).unwrap();
2900 let witness = diagram.conjunction_witness(&[left, right]).unwrap();
2901 let expected = (0..8)
2902 .filter(|&assignment| {
2903 let value = |bit| assignment & (1 << bit) != 0;
2904 diagram.evaluate(left, value) && diagram.evaluate(right, value)
2905 })
2906 .collect::<Vec<_>>();
2907 assert_eq!(witness.is_some(), !expected.is_empty());
2908 if let Some(witness) = witness {
2909 let value = |bit| witness.get(&bit).copied().unwrap_or(false);
2910 assert!(diagram.evaluate(left, value) && diagram.evaluate(right, value));
2911 }
2912 for assignment in 0..8 {
2913 let value = |bit| assignment & (1 << bit) != 0;
2914 let a = diagram.evaluate(left, value);
2915 let b = diagram.evaluate(right, value);
2916 assert_eq!(
2917 operations.map(|root| diagram.evaluate(root, value)),
2918 [a ^ b, a & b, a | b]
2919 );
2920 if b {
2921 assert_eq!(
2922 diagram.evaluate(left, value),
2923 diagram.evaluate(constrained, value)
2924 );
2925 }
2926 }
2927 }
2928 }
2929 }
2930
2931 #[test]
2932 fn collection_preserves_live_functions_and_rebuilds_caches() {
2933 let mut diagram = BooleanDecisionDiagram::default();
2934 let variables = (0..8)
2935 .map(|variable| {
2936 diagram
2937 .make_node(variable, DECISION_FALSE, DECISION_TRUE)
2938 .unwrap()
2939 })
2940 .collect::<Vec<_>>();
2941 let left = diagram
2942 .apply(BooleanOp::And, variables[0], variables[1])
2943 .unwrap();
2944 let right = diagram
2945 .apply(BooleanOp::Xor, variables[2], variables[3])
2946 .unwrap();
2947 let readout = diagram.apply(BooleanOp::Or, left, right).unwrap();
2948 let complement = diagram.negate(readout).unwrap();
2949 let mut garbage = variables[7];
2950 for &variable in variables.iter().rev() {
2951 garbage = diagram.apply(BooleanOp::Xor, variable, garbage).unwrap();
2952 }
2953 let mut roots = [
2954 readout,
2955 complement,
2956 left,
2957 variables[0],
2958 DECISION_FALSE,
2959 DECISION_TRUE,
2960 readout,
2961 ];
2962 let expected = (0..16)
2963 .map(|mask| roots.map(|root| diagram.evaluate(root, |bit| mask & (1 << bit) != 0)))
2964 .collect::<Vec<_>>();
2965 let before = diagram.nodes().len();
2966 let removed = diagram.collect_garbage(roots.iter_mut());
2967 assert!(removed > 0);
2968 assert_eq!(diagram.nodes().len() + removed, before);
2969 assert_eq!(roots[0], roots[6]);
2970 for (mask, expected) in expected.into_iter().enumerate() {
2971 assert_eq!(
2972 roots.map(|root| diagram.evaluate(root, |bit| mask & (1 << bit) != 0)),
2973 expected
2974 );
2975 }
2976 for root in roots {
2977 let complement = diagram.negate(root).unwrap();
2978 assert_eq!(diagram.negate(complement).unwrap(), root);
2979 assert_eq!(
2980 diagram.apply(BooleanOp::Xor, root, complement).unwrap(),
2981 DECISION_TRUE
2982 );
2983 }
2984 let before = diagram.nodes().len();
2985 assert_eq!(diagram.collect_garbage(std::iter::empty()), before);
2986 assert!(diagram.nodes().is_empty());
2987 let variable = diagram.make_node(4, DECISION_FALSE, DECISION_TRUE).unwrap();
2988 assert_eq!(variable, DecisionId(2));
2989 assert!(diagram.evaluate(variable, |bit| bit == 4));
2990 }
2991
2992 #[test]
2993 fn sparse_row_updates_preserve_boolean_coefficients() {
2994 let mut diagram = BooleanDecisionDiagram::default();
2995 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
2996 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
2997 let roots = [
2998 DECISION_FALSE,
2999 DECISION_TRUE,
3000 a,
3001 b,
3002 diagram.negate(a).unwrap(),
3003 ];
3004 for seed in 0..32 {
3005 let terms = (0..80)
3006 .map(|index| {
3007 (
3008 (index * 17 + seed) % 101,
3009 roots[(index + seed) % roots.len()],
3010 )
3011 })
3012 .collect::<Vec<_>>();
3013 let mut row = BooleanRow::from_terms(a, terms.iter().copied());
3014 let original = terms.into_iter().collect::<BTreeMap<_, _>>();
3015 let other = BooleanRow::from_terms(
3016 b,
3017 (0..80).map(|index| {
3018 (
3019 (index * 23 + seed) % 101,
3020 roots[(index * 3 + seed) % roots.len()],
3021 )
3022 }),
3023 );
3024 let factor = roots[seed % roots.len()];
3025 row.xor_scaled(&other, factor, &mut diagram).unwrap();
3026 assert_eq!(row.active, a, "row operations preserve availability");
3027 assert!(row.terms().windows(2).all(|pair| pair[0].0 < pair[1].0));
3028 assert!(
3029 row.terms()
3030 .iter()
3031 .all(|&(_, value)| value != DECISION_FALSE)
3032 );
3033 for assignment in 0..4 {
3034 let eval = |root| diagram.evaluate(root, |bit| assignment & (1 << bit) != 0);
3035 for column in 0..105 {
3036 let left = original.get(&column).copied().unwrap_or(DECISION_FALSE);
3037 assert_eq!(
3038 eval(row.get(column)),
3039 eval(left) ^ (eval(factor) & eval(other.get(column)))
3040 );
3041 }
3042 }
3043 let before = row.clone();
3044 row.map_coefficients(|value| diagram.apply(BooleanOp::And, value, a))
3045 .unwrap();
3046 row.truncate_columns(50);
3047 for column in 0..105 {
3048 let expected = if column < 50 {
3049 diagram
3050 .apply(BooleanOp::And, before.get(column), a)
3051 .unwrap()
3052 } else {
3053 DECISION_FALSE
3054 };
3055 assert_eq!(row.get(column), expected);
3056 }
3057 assert!(
3058 row.terms()
3059 .iter()
3060 .all(|&(column, value)| column < 50 && value != DECISION_FALSE)
3061 );
3062 }
3063 }
3064
3065 #[test]
3066 fn indexed_pivots_match_full_scans_with_conditional_witnesses() {
3067 fn evaluated<'a>(
3068 rows: impl IntoIterator<Item = &'a BooleanRow>,
3069 diagram: &BooleanDecisionDiagram,
3070 ) -> Vec<Vec<bool>> {
3071 rows.into_iter()
3072 .map(|row| {
3073 (0..4)
3074 .flat_map(|assignment| {
3075 std::iter::once(row.active)
3076 .chain((0..28).map(|column| row.get(column)))
3077 .map(move |root| {
3078 diagram.evaluate(root, |bit| assignment & (1 << bit) != 0)
3079 })
3080 })
3081 .collect()
3082 })
3083 .collect()
3084 }
3085 let mut diagram = BooleanDecisionDiagram::default();
3086 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
3087 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
3088 let roots = [
3089 DECISION_FALSE,
3090 DECISION_TRUE,
3091 a,
3092 b,
3093 diagram.negate(a).unwrap(),
3094 diagram.apply(BooleanOp::Xor, a, b).unwrap(),
3095 ];
3096 for seed in 0..roots.len() {
3098 let mut expected = (0..8)
3099 .map(|index| {
3100 let mut row = BooleanRow::from_terms(
3101 roots[(index + seed) % roots.len()],
3102 (0..6).map(|column| {
3103 (
3104 column,
3105 roots[(index * 7 + column * 3 + seed * (column + 1)) % roots.len()],
3106 )
3107 }),
3108 );
3109 row.set(20 + index, DECISION_TRUE);
3110 row
3111 })
3112 .collect::<Vec<_>>();
3113 let mut reference_diagram = diagram.clone();
3114 let mut indexed_diagram = diagram.clone();
3115 let mut indexed = BooleanRowSpace::new(expected.clone(), &mut indexed_diagram).unwrap();
3116 for terms in [
3117 vec![(0, a), (1, b), (0, b), (3, DECISION_FALSE)],
3118 vec![(4, DECISION_TRUE), (2, a)],
3119 vec![(1, b), (1, b)],
3120 ] {
3121 let expected_pivot = eliminate_boolean_rows(
3122 &mut expected,
3123 &mut reference_diagram,
3124 |row, diagram| {
3125 terms
3126 .iter()
3127 .try_fold(DECISION_FALSE, |sum, &(column, weight)| {
3128 let term =
3129 diagram.apply(BooleanOp::And, weight, row.get(column))?;
3130 diagram.apply(BooleanOp::Xor, sum, term)
3131 })
3132 },
3133 )
3134 .unwrap();
3135 let pivot = indexed
3136 .eliminate_linear(&terms, &mut indexed_diagram)
3137 .unwrap();
3138 assert_eq!(
3139 evaluated([&pivot], &indexed_diagram),
3140 evaluated([&expected_pivot], &reference_diagram),
3141 "linear pivot, seed {seed}"
3142 );
3143 assert_eq!(
3144 evaluated(indexed.rows(), &indexed_diagram),
3145 evaluated(&expected, &reference_diagram),
3146 "linear remainder, seed {seed}"
3147 );
3148 }
3149 let mut expected_retained = Vec::<BooleanRow>::new();
3150 let mut retained = BooleanRowSpace::default();
3151 for (column, gate) in [
3152 (4, a),
3153 (1, DECISION_TRUE),
3154 (4, DECISION_TRUE),
3155 (0, b),
3156 (5, DECISION_FALSE),
3157 (5, DECISION_TRUE),
3158 (3, DECISION_TRUE),
3159 (2, DECISION_TRUE),
3160 (0, DECISION_TRUE),
3161 (5, DECISION_TRUE),
3162 ] {
3163 let expected_pivot = eliminate_boolean_rows(
3164 &mut expected,
3165 &mut reference_diagram,
3166 |row, diagram| diagram.apply(BooleanOp::And, gate, row.get(column)),
3167 )
3168 .unwrap();
3169 let pivot = indexed
3170 .eliminate_column_when(column, gate, &mut indexed_diagram)
3171 .unwrap();
3172 assert_eq!(
3173 evaluated([&pivot], &indexed_diagram),
3174 evaluated([&expected_pivot], &reference_diagram),
3175 "pivot {column}, seed {seed}"
3176 );
3177 assert_eq!(
3178 evaluated(indexed.rows(), &indexed_diagram),
3179 evaluated(&expected, &reference_diagram),
3180 "remaining {column}, seed {seed}"
3181 );
3182 if expected_pivot.active != DECISION_FALSE {
3183 for row in &mut expected_retained {
3184 let factor = reference_diagram
3185 .apply(BooleanOp::And, row.active, row.get(column))
3186 .unwrap();
3187 row.xor_scaled(&expected_pivot, factor, &mut reference_diagram)
3188 .unwrap();
3189 }
3190 retained
3191 .reduce_column(column, &pivot, &mut indexed_diagram)
3192 .unwrap();
3193 if [4, 1].contains(&column) {
3196 expected_retained.push(expected_pivot);
3197 retained.push(pivot, &mut indexed_diagram).unwrap();
3198 }
3199 }
3200 assert_eq!(
3201 evaluated(retained.rows(), &indexed_diagram),
3202 evaluated(&expected_retained, &reference_diagram),
3203 "retained {column}, seed {seed}"
3204 );
3205 }
3206 }
3207 }
3208
3209 #[test]
3210 fn indexed_mutations_match_rebuilt_conditional_weighted_spaces() {
3211 let mut diagram = BooleanDecisionDiagram::default();
3212 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
3213 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
3214 let roots = [
3215 DECISION_FALSE,
3216 DECISION_TRUE,
3217 a,
3218 b,
3219 diagram.negate(a).unwrap(),
3220 diagram.apply(BooleanOp::Xor, a, b).unwrap(),
3221 ];
3222 for seed in 0..24 {
3223 let mut indexed = BooleanRowSpace::default();
3224 let mut expected = Vec::<Option<BooleanRow>>::new();
3225 for index in 0..8 {
3226 let mut row = BooleanRow::from_terms(
3227 roots[(index + seed) % roots.len()],
3228 (0..6).map(|column| {
3229 (
3230 column,
3231 roots[(index * 7 + column * 3 + seed * (column + 1)) % roots.len()],
3232 )
3233 }),
3234 );
3235 row.set(20 + index, DECISION_TRUE);
3236 let id = indexed.insert_indexed(row.clone(), &mut diagram).unwrap();
3237 if row.active != DECISION_FALSE {
3238 assert_eq!(id, Some(expected.len()));
3239 expected.push(Some(row));
3240 } else {
3241 assert_eq!(id, None);
3242 }
3243 }
3244 for step in 0..6 {
3245 let id = (seed + step) % expected.len();
3246 let erased = (step * 3 + seed) % 6;
3247 let taken = indexed.take(id, &mut diagram).unwrap();
3248 let mut row = expected[id].take().unwrap();
3249 assert_eq!(taken.as_ref().unwrap().terms(), row.terms());
3250 assert!(indexed.row(id).is_none());
3251 assert!(indexed.take(id, &mut diagram).unwrap().is_none());
3252 indexed.erase_column(erased, &mut diagram).unwrap();
3253 for row in expected.iter_mut().flatten() {
3254 row.set(erased, DECISION_FALSE);
3255 }
3256 row.set(erased, DECISION_FALSE);
3257 let addition = BooleanRow::from_terms(
3260 DECISION_TRUE,
3261 [(erased, roots[2 + step % 4]), (40 + step, DECISION_TRUE)],
3262 );
3263 row.xor_scaled(&addition, roots[2 + (seed + step) % 4], &mut diagram)
3264 .unwrap();
3265 assert!(
3266 indexed
3267 .replace(id, row.clone(), &mut diagram)
3268 .unwrap()
3269 .is_none()
3270 );
3271 expected[id] = Some(row.clone());
3272 row.set(50 + step, a);
3275 let old = indexed
3276 .replace(id, row.clone(), &mut diagram)
3277 .unwrap()
3278 .unwrap();
3279 assert_eq!(old.terms(), expected[id].as_ref().unwrap().terms());
3280 expected[id] = Some(row);
3281 let appended = BooleanRow::from_terms(b, [(erased, a), (60 + step, DECISION_TRUE)]);
3282 assert_eq!(
3283 indexed
3284 .insert_indexed(appended.clone(), &mut diagram)
3285 .unwrap(),
3286 Some(expected.len())
3287 );
3288 expected.push(Some(appended));
3289 assert_eq!(indexed.len(), expected.len());
3290 for (id, row) in expected.iter().enumerate() {
3291 let actual = indexed.row(id).unwrap();
3292 let row = row.as_ref().unwrap();
3293 assert_eq!((actual.active, actual.terms()), (row.active, row.terms()));
3294 }
3295 for column in 0..66 {
3296 assert_eq!(
3297 indexed.row_ids_with_column(column).collect::<Vec<_>>(),
3298 expected
3299 .iter()
3300 .enumerate()
3301 .filter_map(|(id, row)| {
3302 (row.as_ref().unwrap().get(column) != DECISION_FALSE).then_some(id)
3303 })
3304 .collect::<Vec<_>>()
3305 );
3306 }
3307 }
3308 let id = seed % expected.len();
3311 let old = indexed
3312 .replace(id, BooleanRow::new(DECISION_FALSE), &mut diagram)
3313 .unwrap()
3314 .unwrap();
3315 assert_eq!(old.terms(), expected[id].take().unwrap().terms());
3316 let appended = BooleanRow::from_terms(a, [(0, b), (69, DECISION_TRUE)]);
3317 assert_eq!(
3318 indexed
3319 .insert_indexed(appended.clone(), &mut diagram)
3320 .unwrap(),
3321 Some(expected.len())
3322 );
3323 expected.push(Some(appended));
3324 let mut rebuilt =
3325 BooleanRowSpace::new(expected.into_iter().flatten().collect(), &mut diagram)
3326 .unwrap();
3327 for terms in [
3328 vec![(0, a), (1, b), (0, b), (3, DECISION_FALSE)],
3329 vec![(4, DECISION_TRUE), (2, a)],
3330 vec![(1, b), (1, b)],
3331 vec![(0, DECISION_TRUE), (5, DECISION_TRUE)],
3332 ] {
3333 let actual = indexed.eliminate_linear(&terms, &mut diagram).unwrap();
3334 let expected = rebuilt.eliminate_linear(&terms, &mut diagram).unwrap();
3335 assert_eq!(
3336 (actual.active, actual.terms()),
3337 (expected.active, expected.terms())
3338 );
3339 assert_eq!(
3340 indexed
3341 .rows()
3342 .map(|row| (row.active, row.terms()))
3343 .collect::<Vec<_>>(),
3344 rebuilt
3345 .rows()
3346 .map(|row| (row.active, row.terms()))
3347 .collect::<Vec<_>>()
3348 );
3349 }
3350 }
3351 }
3352
3353 #[test]
3354 fn indexed_mutation_limits_leave_rows_and_postings_unchanged() {
3355 for operation in 0..4 {
3356 let mut diagram = BooleanDecisionDiagram::default();
3357 let rows = vec![
3358 BooleanRow::from_terms(DECISION_TRUE, [(0, DECISION_TRUE), (2, DECISION_TRUE)]),
3359 BooleanRow::from_terms(DECISION_TRUE, [(0, DECISION_TRUE), (3, DECISION_TRUE)]),
3360 ];
3361 let mut indexed = BooleanRowSpace::new(rows.clone(), &mut diagram).unwrap();
3362 diagram.limits.max_steps = diagram.steps() + if operation == 0 { 2 } else { 3 };
3364 let error = match operation {
3365 0 => indexed.take(0, &mut diagram),
3366 1 => indexed.replace(0, rows[1].clone(), &mut diagram),
3367 2 => indexed.erase_column(0, &mut diagram).map(|()| None),
3368 _ => indexed
3369 .insert_indexed(
3370 BooleanRow::from_terms(
3371 DECISION_TRUE,
3372 (0..4).map(|column| (column, DECISION_TRUE)),
3373 ),
3374 &mut diagram,
3375 )
3376 .map(|_| None),
3377 };
3378 error.unwrap_err();
3379 assert_eq!(indexed.len(), rows.len());
3380 for (id, row) in rows.iter().enumerate() {
3381 assert_eq!(indexed.row(id).unwrap().terms(), row.terms());
3382 }
3383 assert_eq!(
3384 indexed.row_ids_with_column(0).collect::<Vec<_>>(),
3385 vec![0, 1]
3386 );
3387 assert_eq!(indexed.row_ids_with_column(2).collect::<Vec<_>>(), vec![0]);
3388 assert_eq!(indexed.row_ids_with_column(3).collect::<Vec<_>>(), vec![1]);
3389 }
3390 }
3391
3392 #[test]
3393 fn sparse_pivots_preserve_conditional_augmented_spans() {
3394 fn span<'a>(
3395 rows: impl IntoIterator<Item = &'a BooleanRow>,
3396 diagram: &BooleanDecisionDiagram,
3397 assignment: usize,
3398 ) -> BTreeSet<u32> {
3399 let evaluate = |root| diagram.evaluate(root, |bit| assignment & (1 << bit) != 0);
3400 let mut span = BTreeSet::from([0]);
3401 for row in rows.into_iter().filter(|row| evaluate(row.active)) {
3402 let value = (0..24).fold(0, |value, column| {
3403 value | (u32::from(evaluate(row.get(column))) << column)
3404 });
3405 span.extend(span.iter().map(|prior| prior ^ value).collect::<Vec<_>>());
3406 }
3407 span
3408 }
3409 let mut diagram = BooleanDecisionDiagram::default();
3410 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
3411 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
3412 let roots = [
3413 DECISION_FALSE,
3414 DECISION_TRUE,
3415 a,
3416 b,
3417 diagram.negate(a).unwrap(),
3418 diagram.apply(BooleanOp::Xor, a, b).unwrap(),
3419 ];
3420 for seed in 0..roots.len() {
3422 let rows = (0..8)
3423 .map(|index| {
3424 let mut row = BooleanRow::from_terms(
3425 roots[(index + seed) % roots.len()],
3426 (0..6).map(|column| {
3427 (
3428 column,
3429 roots[(index * 7 + column * 3 + seed * (column + 1)) % roots.len()],
3430 )
3431 }),
3432 );
3433 row.set(16 + index, DECISION_TRUE);
3436 row
3437 })
3438 .collect::<Vec<_>>();
3439 let mut stable = BooleanRowSpace::new(rows.clone(), &mut diagram).unwrap();
3440 let mut sparse = BooleanRowSpace::new(rows, &mut diagram).unwrap();
3441 for (column, terms) in [
3442 (None, vec![(0, a), (1, b), (0, b), (3, DECISION_FALSE)]),
3443 (Some(4), vec![(4, a)]),
3444 (None, vec![(5, DECISION_TRUE), (2, b)]),
3445 (Some(0), vec![(0, DECISION_TRUE)]),
3446 (Some(4), vec![(4, DECISION_TRUE)]),
3447 (None, vec![(1, b), (1, b)]),
3448 ] {
3449 let before = (0..4)
3450 .map(|assignment| span(stable.rows(), &diagram, assignment))
3451 .collect::<Vec<_>>();
3452 let (expected, actual) = if let Some(column) = column {
3453 let gate = terms[0].1;
3454 if gate == DECISION_TRUE {
3455 (
3456 stable.eliminate_column(column, &mut diagram).unwrap(),
3457 sparse
3458 .eliminate_column_sparse(column, &mut diagram)
3459 .unwrap(),
3460 )
3461 } else {
3462 (
3463 stable
3464 .eliminate_column_when(column, gate, &mut diagram)
3465 .unwrap(),
3466 sparse
3467 .eliminate_column_when_sparse(column, gate, &mut diagram)
3468 .unwrap(),
3469 )
3470 }
3471 } else {
3472 (
3473 stable.eliminate_linear(&terms, &mut diagram).unwrap(),
3474 sparse.eliminate_linear(&terms, &mut diagram).unwrap(),
3475 )
3476 };
3477 assert_eq!(actual.active, expected.active);
3478 for (assignment, before) in before.into_iter().enumerate() {
3479 assert_eq!(
3480 span(sparse.rows(), &diagram, assignment),
3481 span(stable.rows(), &diagram, assignment),
3482 "kernel seed {seed}, assignment {assignment}"
3483 );
3484 assert_eq!(
3485 span(sparse.rows().chain([&actual]), &diagram, assignment),
3486 before,
3487 "augmented span seed {seed}, assignment {assignment}"
3488 );
3489 }
3490 }
3491 }
3492 }
3493
3494 #[test]
3495 fn sparse_pivot_ordering_is_charged_and_source_stable_on_ties() {
3496 let rows = vec![
3497 BooleanRow::from_terms(
3498 DECISION_TRUE,
3499 [
3500 (0, DECISION_TRUE),
3501 (1, DECISION_TRUE),
3502 (2, DECISION_TRUE),
3503 (10, DECISION_TRUE),
3504 ],
3505 ),
3506 BooleanRow::from_terms(DECISION_TRUE, [(0, DECISION_TRUE), (11, DECISION_TRUE)]),
3507 BooleanRow::from_terms(DECISION_TRUE, [(0, DECISION_TRUE), (12, DECISION_TRUE)]),
3508 ];
3509 let mut limited = BooleanDecisionDiagram::with_limits(BooleanLimits {
3510 max_nodes: 0,
3511 max_steps: 15,
3512 });
3513 let mut space = BooleanRowSpace::new(rows.clone(), &mut limited).unwrap();
3514 let error = space.eliminate_column_sparse(0, &mut limited).unwrap_err();
3515 assert_eq!(error.resource, "Boolean work steps");
3516 assert_eq!(error.limit, 15);
3517 assert_eq!(
3518 space.rows().map(BooleanRow::terms).collect::<Vec<_>>(),
3519 rows.iter().map(BooleanRow::terms).collect::<Vec<_>>()
3520 );
3521 let mut diagram = BooleanDecisionDiagram::default();
3522 let mut stable = BooleanRowSpace::new(rows.clone(), &mut diagram).unwrap();
3523 let mut sparse = BooleanRowSpace::new(rows.clone(), &mut diagram).unwrap();
3524 assert_eq!(
3525 stable.eliminate_column(0, &mut diagram).unwrap().terms(),
3526 rows[0].terms()
3527 );
3528 assert_eq!(
3529 sparse
3530 .eliminate_column_sparse(0, &mut diagram)
3531 .unwrap()
3532 .terms(),
3533 rows[1].terms()
3534 );
3535 }
3536
3537 #[test]
3538 fn indexed_columns_survive_decision_collection() {
3539 let mut diagram = BooleanDecisionDiagram::default();
3540 diagram
3541 .make_node(99, DECISION_FALSE, DECISION_TRUE)
3542 .unwrap();
3543 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
3544 let mut indexed = BooleanRowSpace::new(
3545 vec![
3546 BooleanRow::from_terms(DECISION_TRUE, [(2, a), (7, DECISION_TRUE)]),
3547 BooleanRow::from_terms(a, [(2, DECISION_TRUE), (9, a)]),
3548 ],
3549 &mut diagram,
3550 )
3551 .unwrap();
3552 let before = diagram.steps();
3553 assert_eq!(diagram.collect_garbage(indexed.decisions_mut()), 1);
3554 assert_eq!(diagram.steps(), before);
3555 assert_eq!(indexed.len(), 2);
3556 assert_eq!(indexed.rows_with_column(2).count(), 2);
3557 assert_eq!(indexed.rows_with_column(7).count(), 1);
3558 let mut reference_diagram = diagram.clone();
3559 let mut reference = indexed.rows().cloned().collect::<Vec<_>>();
3560 let expected = eliminate_boolean_rows(&mut reference, &mut reference_diagram, |row, _| {
3561 Ok(row.get(2))
3562 })
3563 .unwrap();
3564 let pivot = indexed.eliminate_column(2, &mut diagram).unwrap();
3565 for value in [false, true] {
3566 let evaluate = |row: &BooleanRow, diagram: &BooleanDecisionDiagram| {
3567 std::iter::once(row.active)
3568 .chain((0..10).map(|column| row.get(column)))
3569 .map(|root| diagram.evaluate(root, |_| value))
3570 .collect::<Vec<_>>()
3571 };
3572 assert_eq!(
3573 evaluate(&pivot, &diagram),
3574 evaluate(&expected, &reference_diagram)
3575 );
3576 assert_eq!(
3577 indexed
3578 .rows()
3579 .map(|row| evaluate(row, &diagram))
3580 .collect::<Vec<_>>(),
3581 reference
3582 .iter()
3583 .map(|row| evaluate(row, &reference_diagram))
3584 .collect::<Vec<_>>()
3585 );
3586 }
3587 }
3588
3589 #[test]
3590 fn indexed_reduction_scales_with_sparse_incidence() {
3591 let rows = (0..128)
3592 .map(|column| BooleanRow::from_terms(DECISION_TRUE, [(column, DECISION_TRUE)]))
3593 .collect::<Vec<_>>();
3594 let mut diagram = BooleanDecisionDiagram::with_limits(BooleanLimits {
3595 max_nodes: 0,
3596 max_steps: 2_000,
3597 });
3598 let reduced = reduce_boolean_rows(rows, 0..128, &mut diagram).unwrap();
3599 assert_eq!(reduced.len(), 128);
3600 for (column, row) in reduced.iter().enumerate() {
3601 assert_eq!(row.active, DECISION_TRUE);
3602 assert_eq!(row.terms(), [(column, DECISION_TRUE)]);
3603 }
3604 assert!(diagram.steps() < 2_000);
3607 }
3608
3609 #[test]
3610 fn sparse_reduction_skips_absent_and_repeated_columns_in_requested_order() {
3611 let mut diagram = BooleanDecisionDiagram::with_limits(BooleanLimits {
3612 max_steps: 11_000,
3613 ..BooleanLimits::DEFAULT
3614 });
3615 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
3616 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
3617 let not_a = diagram.negate(a).unwrap();
3618 let (low, middle, high) = (10_000, 3, 10_007);
3619 let rows = vec![
3620 BooleanRow::from_terms(a, [(low, DECISION_TRUE), (high, DECISION_TRUE)]),
3621 BooleanRow::from_terms(not_a, [(middle, DECISION_TRUE), (high, DECISION_TRUE)]),
3622 BooleanRow::from_terms(DECISION_TRUE, [(low, DECISION_TRUE), (middle, b)]),
3623 ];
3624 let mut compact_diagram = diagram.clone();
3625 let expected =
3626 reduce_boolean_rows(rows.clone(), [high, middle, low], &mut compact_diagram).unwrap();
3627 let reduced = reduce_boolean_rows(
3628 rows.clone(),
3629 [high, middle, high].into_iter().chain(0..=high),
3630 &mut diagram,
3631 )
3632 .unwrap();
3633 assert_eq!(reduced.len(), 3);
3634 assert_eq!(
3635 reduced[0].get(high),
3636 DECISION_TRUE,
3637 "high column has first priority"
3638 );
3639 assert_eq!(
3640 reduced
3641 .iter()
3642 .map(|row| (row.active, row.terms()))
3643 .collect::<Vec<_>>(),
3644 expected
3645 .iter()
3646 .map(|row| (row.active, row.terms()))
3647 .collect::<Vec<_>>(),
3648 );
3649 assert!(diagram.steps() < 10_200);
3652 for assignment in 0..4 {
3653 let span = |rows: &[BooleanRow]| {
3654 let evaluate = |root| diagram.evaluate(root, |bit| assignment & (1 << bit) != 0);
3655 let mut span = BTreeSet::from([0_u8]);
3656 for row in rows.iter().filter(|row| evaluate(row.active)) {
3657 let value = [high, middle, low]
3658 .into_iter()
3659 .enumerate()
3660 .fold(0, |value, (bit, column)| {
3661 value | (u8::from(evaluate(row.get(column))) << bit)
3662 });
3663 span.extend(span.iter().map(|prior| prior ^ value).collect::<Vec<_>>());
3664 }
3665 span
3666 };
3667 assert_eq!(span(&reduced), span(&rows));
3668 }
3669 }
3670
3671 #[test]
3672 fn reduction_preserves_every_concrete_span() {
3673 let mut diagram = BooleanDecisionDiagram::default();
3674 let a = diagram.make_node(0, DECISION_FALSE, DECISION_TRUE).unwrap();
3675 let b = diagram.make_node(1, DECISION_FALSE, DECISION_TRUE).unwrap();
3676 let roots = [
3677 DECISION_FALSE,
3678 DECISION_TRUE,
3679 a,
3680 b,
3681 diagram.negate(a).unwrap(),
3682 diagram.apply(BooleanOp::And, a, b).unwrap(),
3683 diagram.apply(BooleanOp::Xor, a, b).unwrap(),
3684 ];
3685 for seed in 0..roots.len() {
3686 let rows = (0..5)
3687 .map(|row| BooleanRow {
3688 active: roots[(seed + row * 3) % roots.len()],
3689 bits: (0..4)
3690 .map(|col| (col, roots[(seed * (col + 1) + row * 2) % roots.len()]))
3691 .filter(|(_, value)| *value != DECISION_FALSE)
3692 .collect(),
3693 witness: RowWitness::Explicit,
3694 })
3695 .collect::<Vec<_>>();
3696 let reduced = reduce_boolean_rows(rows.clone(), 0..4, &mut diagram).unwrap();
3697 for assignment in 0..4 {
3698 let span = |rows: &[BooleanRow]| {
3699 let evaluate =
3700 |root| diagram.evaluate(root, |bit| assignment & (1 << bit) != 0);
3701 let mut span = BTreeSet::from([0_u8]);
3702 for row in rows.iter().filter(|row| evaluate(row.active)) {
3703 let value = (0..4).fold(0, |value, col| {
3704 value | (u8::from(evaluate(row.get(col))) << col)
3705 });
3706 span.extend(span.iter().map(|prior| prior ^ value).collect::<Vec<_>>());
3707 }
3708 span
3709 };
3710 assert_eq!(
3711 span(&rows),
3712 span(&reduced),
3713 "seed {seed}, assignment {assignment}"
3714 );
3715 }
3716 }
3717 }
3718}