Skip to main content

bloq_utils/
boolean.rs

1//! Reduced ordered Boolean decision diagrams shared by graph and IR guards.
2use rustc_hash::{FxHashMap, FxHashSet};
3use std::collections::{BTreeMap, BTreeSet};
4
5/// A commutative binary operation over Boolean decision functions.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum BooleanOp {
8    /// Exclusive OR.
9    Xor,
10    /// Conjunction.
11    And,
12    /// Disjunction.
13    Or,
14}
15
16/// Stable handle to a function in a [`BooleanDecisionDiagram`].
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct DecisionId(pub usize);
19/// The constant-false decision.
20pub const DECISION_FALSE: DecisionId = DecisionId(0);
21/// The constant-true decision.
22pub const DECISION_TRUE: DecisionId = DecisionId(1);
23
24/// One nonterminal node in a reduced ordered decision diagram.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub struct DecisionNode {
27    /// Variable tested by this node.
28    pub variable: usize,
29    /// Function selected when the variable is false.
30    pub low: DecisionId,
31    /// Function selected when the variable is true.
32    pub high: DecisionId,
33}
34
35/// Bounds on live decision nodes and cumulative Boolean/row work.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct BooleanLimits {
38    /// Nonterminal arena entries, including intermediates until collection.
39    pub max_nodes: usize,
40    /// Uncached visits, new nodes, and processed row/query coordinates.
41    pub max_steps: usize,
42}
43
44impl BooleanLimits {
45    /// Default production limits.
46    pub const DEFAULT: Self = Self {
47        max_nodes: 4_000_000,
48        max_steps: 64_000_000,
49    };
50    /// Limits that accept every representable allocation and work count.
51    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/// A Boolean operation exceeded its configured resource allowance.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
65#[error("{resource} requires {observed}, limit is {limit}")]
66pub struct BooleanResourceError {
67    /// Resource whose allowance was exceeded.
68    pub resource: &'static str,
69    /// Required or observed resource count.
70    pub observed: usize,
71    /// Configured maximum count.
72    pub limit: usize,
73}
74
75// Caches may forget results; exhaustion must never change Boolean semantics.
76const MAX_CACHE_ENTRIES: usize = 262_144;
77
78/// Reduced ordered Boolean decision diagram with bounded construction work.
79#[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    /// Creates an empty diagram with explicit resource limits.
92    pub fn with_limits(limits: BooleanLimits) -> Self {
93        Self {
94            limits,
95            ..Self::default()
96        }
97    }
98
99    /// Returns the configured resource limits.
100    pub fn limits(&self) -> BooleanLimits {
101        self.limits
102    }
103
104    /// Returns cumulative charged work.
105    pub fn steps(&self) -> usize {
106        self.steps
107    }
108
109    /// Charge before traversal or allocation. Collection and cloning preserve
110    /// consumed work; an error never stands in for a Boolean result.
111    /// Cheap constant/cache returns do not consume steps. Callers must charge
112    /// their own loops, including loops over constant Boolean expressions.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`BooleanResourceError`] if charging `steps` exceeds the work limit.
117    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    /// Rebuild the diagram from all live roots, removing dead intermediate
131    /// functions and operation caches. Variable indices do not change.
132    ///
133    /// Every surviving `DecisionId` must be passed here: ids are renumbered,
134    /// and any unlisted external ids become invalid. Collect only at a boundary
135    /// where the caller owns the complete root set.
136    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        // A node is allocated after its children. Retaining this order also
175        // preserves the relative order of live ids used by deterministic passes.
176        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        // A cached negation is usable only when both functions survived.
193        // The cache does not keep either endpoint alive on its own.
194        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    /// Evaluate one Boolean function without traversing unrelated choices.
211    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    /// One satisfying variable assignment; omitted variables are don't-cares.
223    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    /// Finds one assignment satisfying every supplied decision.
237    ///
238    /// # Errors
239    ///
240    /// Returns [`BooleanResourceError`] if the search exceeds the work limit.
241    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    /// Returns every variable reachable from `root`.
311    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    /// Simplify a function outside its care set. The returned function equals
327    /// `root` wherever `care` holds; impossible selector combinations are free.
328    ///
329    /// # Errors
330    ///
331    /// Returns [`BooleanResourceError`] if construction exceeds configured limits.
332    ///
333    /// # Panics
334    ///
335    /// Panics if either decision does not belong to this diagram.
336    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    /// Returns the nonterminal nodes in decision-id order.
416    pub fn nodes(&self) -> &[DecisionNode] {
417        &self.nodes
418    }
419    /// Returns the reduced node for `variable`, `low`, and `high`.
420    ///
421    /// # Errors
422    ///
423    /// Returns [`BooleanResourceError`] if allocation exceeds configured limits.
424    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    /// Returns the nonterminal node named by `id`, or `None` for a terminal.
457    ///
458    /// # Panics
459    ///
460    /// Panics if `id` names a nonterminal outside this diagram.
461    pub fn node(&self, id: DecisionId) -> Option<DecisionNode> {
462        id.0.checked_sub(2).map(|index| self.nodes[index])
463    }
464
465    /// Returns an already known exact complement without constructing nodes.
466    /// Constants always have known complements.
467    ///
468    /// # Panics
469    ///
470    /// Panics if `root` does not belong to this diagram.
471    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    /// Returns the logical negation of `root`.
483    ///
484    /// # Errors
485    ///
486    /// Returns [`BooleanResourceError`] if construction exceeds configured limits.
487    ///
488    /// # Panics
489    ///
490    /// Panics if `root` does not belong to this diagram.
491    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    /// Fold constants before visiting decision pairs. All operators commute.
523    ///
524    /// # Errors
525    ///
526    /// Returns [`BooleanResourceError`] if construction exceeds configured limits.
527    #[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    /// Enumerate at most `limit` distinct output tuples, charging residual
620    /// states and their coordinates before allocating them.
621    ///
622    /// # Errors
623    ///
624    /// Returns [`BooleanResourceError`] if enumeration exceeds the work limit.
625    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/// A sparse GF(2) row whose entries and availability are Boolean functions.
679/// Extra columns can carry witnesses through exactly the same row operations.
680#[derive(Debug, Clone)]
681pub struct BooleanRow {
682    /// Decision under which this row is available.
683    pub active: DecisionId,
684    // Sorted, unique columns with nonzero coefficients. Row algebra merges
685    // contiguous terms instead of allocating and searching a tree per term.
686    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    // Conservative inclusive bounds; cancellation may shrink actual support.
710    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    // Callers reserve work and check the operation bound before changing a row.
739    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        // ponytail: queries replay ancestry independently; batch fixed roots if
786        // overlapping query histories dominate after planner migration.
787        // Parents are newer than children. Coalesce every path's scale before
788        // visiting a shared child; never materialize intermediate witness rows.
789        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    /// Propagate each requested coordinate through shared transfer uses once.
869    /// Only nonzero contributions enter the current coordinate's worklist;
870    /// independent components do not form a row-by-column scan.
871    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    /// Start an owned tape for passive row coefficients. Every deferred row
1012    /// shares this explicit-column partition; keep every row-space constraint
1013    /// column explicit until all rows are expanded. Passive coordinates can be
1014    /// read with [`BooleanRow::probe_coefficients`]. Operation bounds include
1015    /// dead history, and construction/reconstruction spend the same work meter.
1016    ///
1017    /// # Errors
1018    ///
1019    /// Returns [`BooleanResourceError`] if indexing columns exceeds configured limits.
1020    ///
1021    /// # Panics
1022    ///
1023    /// Panics if a witness tape is already active.
1024    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    /// Promote coordinates into the shared explicit row front without expanding
1052    /// other coefficients. The caller must supply every surviving deferred row
1053    /// owned by this tape, including rows retained for later queries or replay.
1054    /// No deferred clone outside `rows` may be used after this boundary. Explicit
1055    /// rows may also be supplied and remain unchanged.
1056    ///
1057    /// Raw coefficients, including off-activation values, are queried under the
1058    /// old partition. Row fronts and the partition change only after all work
1059    /// succeeds; a failure retains the tape and rows without refunding spent work.
1060    /// Activation and witness handles never change, and columns are never demoted.
1061    /// Fixed roots share one topological evaluation per new column; intermediate
1062    /// values for different columns are never stored together.
1063    ///
1064    /// # Errors
1065    ///
1066    /// Returns [`BooleanResourceError`] if querying or staging exceeds resource limits.
1067    ///
1068    /// # Panics
1069    ///
1070    /// Panics if no witness tape is active.
1071    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    /// Additional owned tape nodes and decision handles walked by collection.
1149    /// Charge this together with the arena and external roots before collecting.
1150    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    /// Release the tape after every retained deferred row has been expanded.
1157    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    /// Query raw coefficients without expanding a deferred row. Results retain
1197    /// request order and duplicates, with zero for absent columns. Activation
1198    /// does not mask the values, including values outside the reachable domain.
1199    /// The row and its composed witness remain unchanged.
1200    ///
1201    /// Returned decisions belong to `diagram`; retain them as roots if collecting
1202    /// before consuming them. No query cache or per-row expanded image is kept.
1203    ///
1204    /// # Errors
1205    ///
1206    /// Returns [`BooleanResourceError`] if querying exceeds cumulative work limits.
1207    ///
1208    /// # Panics
1209    ///
1210    /// Panics if a deferred row has no active witness tape.
1211    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    /// Move coefficients outside the tape's common explicit partition into an
1274    /// immutable witness leaf. This preserves the raw off-activation values.
1275    /// A fresh core allocation releases the former full-support capacity.
1276    ///
1277    /// # Errors
1278    ///
1279    /// Returns [`BooleanResourceError`] if recording the witness exceeds configured limits.
1280    ///
1281    /// # Panics
1282    ///
1283    /// Panics if the row is already deferred or no witness tape is active.
1284    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    /// Restore every deferred coefficient before full-support iteration,
1331    /// mapping, or physical binding. No intermediate witness rows are expanded;
1332    /// use [`Self::probe_coefficients`] to query a subset without changing the row.
1333    ///
1334    /// # Errors
1335    ///
1336    /// Returns [`BooleanResourceError`] if reconstruction exceeds configured limits.
1337    ///
1338    /// # Panics
1339    ///
1340    /// Panics if a deferred row has no active witness tape.
1341    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    /// Creates an empty row available under `active`.
1384    pub fn new(active: DecisionId) -> Self {
1385        Self {
1386            active,
1387            bits: Vec::new(),
1388            witness: RowWitness::Explicit,
1389        }
1390    }
1391
1392    /// Build a row; repeated columns keep their last value, like `set`.
1393    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    /// Explicit nonzero coefficients in strictly increasing column order.
1405    /// Deferred witness coefficients are restored by `expand_coefficients`.
1406    pub fn terms(&self) -> &[(usize, DecisionId)] {
1407        &self.bits
1408    }
1409
1410    /// Consumes the row and returns its explicit coefficients.
1411    ///
1412    /// # Panics
1413    ///
1414    /// Panics if deferred coefficients have not been expanded.
1415    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    /// Replace coefficients and remove terms that become zero.
1425    ///
1426    /// # Errors
1427    ///
1428    /// Returns the first [`BooleanResourceError`] produced by `map`.
1429    ///
1430    /// # Panics
1431    ///
1432    /// Panics if deferred coefficients have not been expanded.
1433    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    /// Discard auxiliary columns at and above `end`.
1456    ///
1457    /// # Panics
1458    ///
1459    /// Panics if deferred coefficients have not been expanded.
1460    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    /// Every decision owned by this row, for arena lifetime management.
1471    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    /// Query an explicit coefficient; deferred columns must not be constraints.
1476    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    /// Sets one explicit coefficient, removing it when `value` is false.
1483    ///
1484    /// # Panics
1485    ///
1486    /// Panics if the row has deferred coefficients.
1487    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    /// XORs `other` into this row under the Boolean `factor`.
1513    ///
1514    /// # Errors
1515    ///
1516    /// Returns [`BooleanResourceError`] if row algebra exceeds configured limits.
1517    ///
1518    /// # Panics
1519    ///
1520    /// Panics if explicit coefficients are mixed with deferred witness rows.
1521    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        // Preserve untouched prefixes/suffixes and their allocation. Copying
1561        // untouched growing witnesses at each local seam adds quadratic work.
1562        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/// A sparse conditional row space with stable source order and column incidence.
1606///
1607/// Row slots stay fixed while pivots remove rows. Default methods visit only
1608/// possible contributors in source order; explicit sparse variants prefer
1609/// shorter rows when individual representatives may change.
1610#[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    /// Builds an indexed row space in source order.
1619    ///
1620    /// # Errors
1621    ///
1622    /// Returns [`BooleanResourceError`] if indexing exceeds configured limits.
1623    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    /// Returns the number of live rows.
1635    pub fn len(&self) -> usize {
1636        self.live
1637    }
1638
1639    /// Returns whether the space contains no live rows.
1640    pub fn is_empty(&self) -> bool {
1641        self.live == 0
1642    }
1643
1644    /// Roots for diagram collection. Only renumber the same truth functions;
1645    /// changing coefficients or activation here would invalidate the index.
1646    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    /// Iterates live rows in source order.
1654    pub fn rows(&self) -> impl Iterator<Item = &BooleanRow> {
1655        self.rows.iter().filter_map(Option::as_ref)
1656    }
1657
1658    /// Live rows with their stable insertion IDs, in source order.
1659    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    /// Slots traversed by `indexed_rows`, including removed row IDs.
1667    pub fn indexed_slot_count(&self) -> usize {
1668        self.rows.len()
1669    }
1670
1671    /// A row by its stable insertion ID, or `None` if it has been removed.
1672    pub fn row(&self, index: usize) -> Option<&BooleanRow> {
1673        self.rows.get(index).and_then(Option::as_ref)
1674    }
1675
1676    /// Possible support columns, in unspecified order.
1677    pub fn columns(&self) -> impl ExactSizeIterator<Item = usize> + '_ {
1678        self.columns.keys().copied()
1679    }
1680
1681    /// Rows whose active support may contain `column`, in source order.
1682    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    /// Stable IDs whose active support may contain `column`, in source order.
1688    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    /// Append a row after all existing rows in the pivot priority order.
1697    ///
1698    /// # Errors
1699    ///
1700    /// Returns [`BooleanResourceError`] if indexing exceeds configured limits.
1701    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    /// Append a row and return its stable ID. Inactive rows are discarded.
1710    /// New insertions never reuse removed IDs; `replace` can restore a known ID.
1711    ///
1712    /// # Errors
1713    ///
1714    /// Returns [`BooleanResourceError`] if indexing exceeds configured limits.
1715    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    /// Remove a row and its postings, retaining its slot for explicit replacement.
1738    ///
1739    /// # Errors
1740    ///
1741    /// Returns [`BooleanResourceError`] if accounting exceeds configured limits.
1742    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    /// Replace an existing slot without changing its pivot priority or ID.
1756    /// An inactive replacement removes the row. Returns the displaced row.
1757    ///
1758    /// # Panics
1759    /// Panics if `index` was never returned by `insert_indexed` in this space.
1760    ///
1761    /// # Errors
1762    ///
1763    /// Returns [`BooleanResourceError`] if accounting exceeds configured limits.
1764    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    /// Erase a column from every indexed row without changing activation.
1790    /// After unconditional elimination, off-activation coefficients may remain
1791    /// unindexed; this guarantees removal from active support. Spaces maintained
1792    /// by insertion, replacement, and reduction retain full raw postings.
1793    ///
1794    /// # Errors
1795    ///
1796    /// Returns [`BooleanResourceError`] if accounting exceeds configured limits.
1797    ///
1798    /// # Panics
1799    ///
1800    /// Panics if the internal row-to-column index is inconsistent.
1801    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        // Removing a sorted coefficient can shift its suffix. Charge all rows
1820        // before changing any of them or taking ownership of the postings.
1821        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    /// Consumes the space and returns its live rows in source order.
1832    ///
1833    /// # Errors
1834    ///
1835    /// Returns [`BooleanResourceError`] if accounting exceeds configured limits.
1836    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        // Only pivot columns can change membership. Charge index maintenance
1886        // before mutating the row, so exhaustion cannot leave stale postings.
1887        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    /// Eliminate one column pointwise, choosing the first available row on each
1908    /// guard. The returned pivot carries every auxiliary witness coordinate.
1909    ///
1910    /// # Errors
1911    ///
1912    /// Returns [`BooleanResourceError`] if elimination exceeds configured limits.
1913    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    /// Eliminate one column using shortest-support pivots, with source order
1922    /// breaking ties. Preserves the conditional row space, not its representatives.
1923    ///
1924    /// # Errors
1925    ///
1926    /// Returns [`BooleanResourceError`] if elimination exceeds configured limits.
1927    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    /// Eliminate `gate & coefficient`, retaining the column outside the gate.
1936    ///
1937    /// # Errors
1938    ///
1939    /// Returns [`BooleanResourceError`] if elimination exceeds configured limits.
1940    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    /// Guarded column elimination with shortest-support pivots. Individual
1950    /// representatives may change; support outside the gate remains available.
1951    ///
1952    /// # Errors
1953    ///
1954    /// Returns [`BooleanResourceError`] if elimination exceeds configured limits.
1955    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        // Any surviving coefficient is outside its row's activation. Later
1982        // pivots mask those coefficients out, so this column stays eliminated.
1983        if gate == DECISION_TRUE {
1984            self.columns.remove(&column);
1985        }
1986        Ok(pivot)
1987    }
1988
1989    /// Eliminate an ordered XOR of weighted column coefficients. Repeated
1990    /// columns retain every occurrence; columns themselves need not become zero.
1991    ///
1992    /// # Errors
1993    ///
1994    /// Returns [`BooleanResourceError`] if elimination exceeds configured limits.
1995    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        // Account for comparison-sort work before sorting or changing any row.
2032        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    /// Reduce only rows that may carry this pivot column. Unlike elimination,
2082    /// this preserves their activation and retains their other pivot choices.
2083    ///
2084    /// # Errors
2085    ///
2086    /// Returns [`BooleanResourceError`] if reduction exceeds configured limits.
2087    ///
2088    /// # Panics
2089    ///
2090    /// Panics if the internal row-to-column index is inconsistent.
2091    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    /// Reduce an ordered XOR of weighted columns against a supplied pivot.
2110    /// The pivot must have unit projection on its activation.
2111    ///
2112    /// # Errors
2113    /// Returns [`BooleanResourceError`] if reduction exceeds configured limits.
2114    ///
2115    /// # Panics
2116    /// Panics if the internal row-to-column index is inconsistent.
2117    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
2149/// Splice `row` into `pivot` on the guards where `pivot` is not yet available,
2150/// and withdraw those guards from `row`. The pivot therefore takes the first
2151/// available row pointwise, and the guards the two carry stay disjoint.
2152///
2153/// Shared by the dense [`eliminate_boolean_rows`] and the indexed
2154/// [`BooleanRowSpace::eliminate_candidates`] so their pivot choice, sign
2155/// handling, and charged work cannot drift apart.
2156fn 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
2172/// Eliminate one linear constraint pointwise, returning its conditional pivot.
2173///
2174/// The first available pivot is selected by disjoint guards, so no assignment
2175/// enumeration or product of alternative row spaces is needed.
2176///
2177/// # Errors
2178///
2179/// Returns [`BooleanResourceError`] if elimination exceeds configured limits.
2180pub 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
2204/// Canonical reduced rows over the requested columns, with conditional rank.
2205///
2206/// # Errors
2207///
2208/// Returns [`BooleanResourceError`] if reduction exceeds configured limits.
2209pub 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        // The first row's query cancels on both branches; the second is
2260        // selected exactly when its activation is true. Column 2 cancels twice.
2261        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                // The tape operation fits; the following explicit merge does not.
2305                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        // Every work-limit cut, including failure after staged row construction.
2679        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        // Every seed-dependent coefficient repeats modulo roots.len().
3097        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                    // Retaining only named pivots must give the same named rows
3194                    // as full scans while physical/kernel pivots are discarded.
3195                    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                // A conditional update reintroduces an erased coordinate and
3258                // changes source witnesses before restoring the same row ID.
3259                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                // Replacement of a live row must drop old postings, add new
3273                // ones, and keep its place ahead of later insertions.
3274                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            // Drop one slot through replacement, then append without recycling
3309            // its ID. A fresh space has the same source order despite the hole.
3310            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            // Enough for lookup/candidate counting, but not the actual mutation.
3363            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        // Every seed-dependent coefficient repeats modulo roots.len().
3421        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                    // Source coordinates make this stronger than a physical-only
3434                    // span check: every reconstruction coefficient is retained.
3435                    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        // Full-row forward/backward scans need quadratic work even though all
3605        // rows are disjoint. Incidence construction and each pivot are linear.
3606        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        // The wide range still costs one lookup per requested column, but no
3650        // whole-row scans or factor vectors for its ten thousand empty columns.
3651        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}