1use crate::cdcl::Lit;
23use crate::xorsat::XorEquation;
24
25#[derive(Clone)]
28struct Row {
29 vars: Vec<u64>,
31 rhs: bool,
33 prov: Vec<u64>,
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
39pub enum XorStep {
40 Conflict(Vec<Lit>),
43 Imply { lit: Lit, reason: Vec<Lit> },
45 Sat,
47}
48
49pub struct XorEngine {
51 num_vars: usize,
52 var_words: usize,
53 eq_words: usize,
54 orig_vars: Vec<Vec<u64>>,
56 orig_rhs: Vec<bool>,
57}
58
59#[inline]
60fn words_for(n: usize) -> usize {
61 n.div_ceil(64).max(1)
62}
63#[inline]
64fn bit_set(bits: &mut [u64], i: usize) {
65 bits[i / 64] |= 1u64 << (i % 64);
66}
67#[inline]
68fn bit_get(bits: &[u64], i: usize) -> bool {
69 (bits[i / 64] >> (i % 64)) & 1 == 1
70}
71#[inline]
72fn xor_into(dst: &mut [u64], src: &[u64]) {
73 for (d, s) in dst.iter_mut().zip(src) {
74 *d ^= *s;
75 }
76}
77#[inline]
78fn bit_clear(bits: &mut [u64], i: usize) {
79 bits[i / 64] &= !(1u64 << (i % 64));
80}
81#[inline]
83fn occ_remove(list: &mut Vec<usize>, row: usize) {
84 if let Some(p) = list.iter().position(|&r| r == row) {
85 list.swap_remove(p);
86 }
87}
88#[inline]
89fn is_zero(bits: &[u64]) -> bool {
90 bits.iter().all(|&w| w == 0)
91}
92#[inline]
93fn popcount(bits: &[u64]) -> u32 {
94 bits.iter().map(|w| w.count_ones()).sum()
95}
96fn lowest_set(bits: &[u64]) -> Option<usize> {
98 for (wi, &w) in bits.iter().enumerate() {
99 if w != 0 {
100 return Some(wi * 64 + w.trailing_zeros() as usize);
101 }
102 }
103 None
104}
105fn set_bits(bits: &[u64]) -> Vec<usize> {
106 let mut out = Vec::new();
107 for (wi, &w) in bits.iter().enumerate() {
108 let mut w = w;
109 while w != 0 {
110 let b = w.trailing_zeros() as usize;
111 out.push(wi * 64 + b);
112 w &= w - 1;
113 }
114 }
115 out
116}
117
118impl XorEngine {
119 pub fn new(num_vars: usize, eqs: &[XorEquation]) -> Self {
122 let var_words = words_for(num_vars);
123 let eq_words = words_for(eqs.len());
124 let mut orig_vars = Vec::with_capacity(eqs.len());
125 let mut orig_rhs = Vec::with_capacity(eqs.len());
126 for e in eqs {
127 let mut vars = vec![0u64; var_words];
128 let mut ok = true;
129 for &v in &e.vars {
130 if v >= num_vars {
131 ok = false;
132 break;
133 }
134 bit_set(&mut vars, v);
135 }
136 if !ok {
137 continue;
138 }
139 orig_vars.push(vars);
140 orig_rhs.push(e.rhs);
141 }
142 XorEngine { num_vars, var_words, eq_words, orig_vars, orig_rhs }
143 }
144
145 pub fn is_active(&self) -> bool {
147 !self.orig_vars.is_empty()
148 }
149
150 fn derived(&self, prov: &[u64]) -> (Vec<u64>, bool) {
152 let mut vars = vec![0u64; self.var_words];
153 let mut rhs = false;
154 for i in set_bits(prov) {
155 xor_into(&mut vars, &self.orig_vars[i]);
156 rhs ^= self.orig_rhs[i];
157 }
158 (vars, rhs)
159 }
160
161 fn gadget_clause(
168 &self,
169 dv: &[u64],
170 assign: &[Option<bool>],
171 forced: Option<(usize, bool)>,
172 ) -> Vec<Lit> {
173 let mut clause = Vec::new();
174 for u in set_bits(dv) {
175 match forced {
176 Some((fv, fval)) if fv == u => clause.push(Lit::new(u as u32, fval)),
179 _ => {
183 let val = assign[u].expect("gadget var must be assigned");
184 clause.push(Lit::new(u as u32, !val));
185 }
186 }
187 }
188 clause
189 }
190
191 pub fn analyze(&self, assign: &[Option<bool>]) -> XorStep {
196 match self.all_consequences(assign) {
197 Err(conflict) => XorStep::Conflict(conflict),
198 Ok(forced) => match forced.into_iter().next() {
199 Some((lit, reason)) => XorStep::Imply { lit, reason },
200 None => XorStep::Sat,
201 },
202 }
203 }
204
205 pub fn all_consequences(&self, assign: &[Option<bool>]) -> Result<Vec<(Lit, Vec<Lit>)>, Vec<Lit>> {
208 let mut rows: Vec<Row> = Vec::with_capacity(self.orig_vars.len());
211 for (i, ov) in self.orig_vars.iter().enumerate() {
212 let mut vars = vec![0u64; self.var_words];
213 let mut rhs = self.orig_rhs[i];
214 for v in set_bits(ov) {
215 match assign[v] {
216 Some(val) => rhs ^= val, None => bit_set(&mut vars, v), }
219 }
220 let mut prov = vec![0u64; self.eq_words];
221 bit_set(&mut prov, i);
222 rows.push(Row { vars, rhs, prov });
223 }
224
225 let mut basis: Vec<Row> = Vec::new();
228 let mut pivot_at: Vec<Option<usize>> = vec![None; self.num_vars];
229 let mut forced: Vec<(Lit, Vec<Lit>)> = Vec::new();
230
231 for mut row in rows {
232 loop {
235 let pivot_col = set_bits(&row.vars).into_iter().find(|&p| pivot_at[p].is_some());
236 match pivot_col {
237 Some(p) => {
238 let b = &basis[pivot_at[p].unwrap()];
239 xor_into(&mut row.vars, &b.vars);
240 row.rhs ^= b.rhs;
241 xor_into(&mut row.prov, &b.prov);
242 }
243 None => break,
244 }
245 }
246 if is_zero(&row.vars) {
247 if row.rhs {
248 let (dv, _) = self.derived(&row.prov);
250 return Err(self.gadget_clause(&dv, assign, None));
251 }
252 continue; }
254 let p = lowest_set(&row.vars).unwrap();
255 for b in basis.iter_mut() {
259 if bit_get(&b.vars, p) {
260 xor_into(&mut b.vars, &row.vars);
261 b.rhs ^= row.rhs;
262 xor_into(&mut b.prov, &row.prov);
263 }
264 }
265 pivot_at[p] = Some(basis.len());
266 basis.push(row);
267 }
268
269 for row in &basis {
271 if popcount(&row.vars) == 1 {
272 let v = lowest_set(&row.vars).unwrap();
273 let val = row.rhs;
274 let (dv, _) = self.derived(&row.prov);
275 let reason = self.gadget_clause(&dv, assign, Some((v, val)));
276 forced.push((Lit::new(v as u32, val), reason));
277 }
278 }
279 Ok(forced)
280 }
281}
282
283pub struct IncXor {
293 num_vars: usize,
294 var_words: usize,
295 orig_vars: Vec<Vec<u64>>,
296 orig_rhs: Vec<bool>,
297 rows: Vec<IncRow>,
298 pivot_row: Vec<Option<usize>>,
300 occ: Vec<Vec<usize>>,
305 weight: Vec<u32>,
309 low: Vec<usize>,
313 in_low: Vec<bool>,
314 value: Vec<Option<bool>>,
315 applied: Vec<usize>,
316 undo: Vec<Frame>,
317}
318
319#[derive(Clone)]
320struct IncRow {
321 bits: Vec<u64>,
322 rhs: bool,
323 prov: Vec<u64>,
324}
325
326struct Frame {
328 var: usize,
329 rows: Vec<(usize, IncRow)>,
330 pivots: Vec<(usize, Option<usize>)>,
331 touched_rows: std::collections::HashSet<usize>,
332 touched_pivots: std::collections::HashSet<usize>,
333}
334
335impl IncXor {
336 pub fn new(num_vars: usize, eqs: &[XorEquation]) -> Self {
338 let var_words = words_for(num_vars);
339 let eq_words = words_for(eqs.len());
340 let mut orig_vars = Vec::new();
341 let mut orig_rhs = Vec::new();
342 let mut staged: Vec<IncRow> = Vec::new();
343 for e in eqs {
344 let mut bits = vec![0u64; var_words];
345 let mut ok = true;
346 for &v in &e.vars {
347 if v >= num_vars {
348 ok = false;
349 break;
350 }
351 bit_set(&mut bits, v);
352 }
353 if !ok {
354 continue;
355 }
356 let idx = orig_vars.len();
357 let mut prov = vec![0u64; eq_words];
358 bit_set(&mut prov, idx);
359 orig_vars.push(bits.clone());
360 orig_rhs.push(e.rhs);
361 staged.push(IncRow { bits, rhs: e.rhs, prov });
362 }
363
364 let mut rows: Vec<IncRow> = Vec::new();
366 let mut pivot_row: Vec<Option<usize>> = vec![None; num_vars];
367 for mut row in staged {
368 loop {
369 let pc = set_bits(&row.bits).into_iter().find(|&p| pivot_row[p].is_some());
370 match pc {
371 Some(p) => {
372 let b = rows[pivot_row[p].unwrap()].clone();
373 xor_into(&mut row.bits, &b.bits);
374 row.rhs ^= b.rhs;
375 xor_into(&mut row.prov, &b.prov);
376 }
377 None => break,
378 }
379 }
380 if is_zero(&row.bits) {
381 if row.rhs {
382 rows.push(row); }
384 continue;
385 }
386 let p = lowest_set(&row.bits).unwrap();
387 let idx = rows.len();
388 for b in rows.iter_mut() {
389 if bit_get(&b.bits, p) {
390 xor_into(&mut b.bits, &row.bits);
391 b.rhs ^= row.rhs;
392 xor_into(&mut b.prov, &row.prov);
393 }
394 }
395 pivot_row[p] = Some(idx);
396 rows.push(row);
397 }
398
399 let mut occ = vec![Vec::new(); num_vars];
400 let mut weight = vec![0u32; rows.len()];
401 let mut low = Vec::new();
402 let mut in_low = vec![false; rows.len()];
403 for (ri, row) in rows.iter().enumerate() {
404 for v in set_bits(&row.bits) {
405 occ[v].push(ri);
406 }
407 weight[ri] = popcount(&row.bits);
408 if weight[ri] <= 1 {
409 low.push(ri);
410 in_low[ri] = true;
411 }
412 }
413
414 IncXor {
415 num_vars,
416 var_words,
417 orig_vars,
418 orig_rhs,
419 rows,
420 pivot_row,
421 occ,
422 weight,
423 low,
424 in_low,
425 value: vec![None; num_vars],
426 applied: Vec::new(),
427 undo: Vec::new(),
428 }
429 }
430
431 #[inline]
433 fn note_weight(&mut self, r: usize) {
434 if self.weight[r] <= 1 && !self.in_low[r] {
435 self.in_low[r] = true;
436 self.low.push(r);
437 }
438 }
439
440 pub fn is_active(&self) -> bool {
441 !self.orig_vars.is_empty()
442 }
443
444 pub fn assigned_len(&self) -> usize {
445 self.applied.len()
446 }
447
448 fn snap_row(frame: &mut Frame, rows: &[IncRow], i: usize) {
449 if frame.touched_rows.insert(i) {
450 frame.rows.push((i, rows[i].clone()));
451 }
452 }
453 fn snap_pivot(frame: &mut Frame, pivot_row: &[Option<usize>], v: usize) {
454 if frame.touched_pivots.insert(v) {
455 frame.pivots.push((v, pivot_row[v]));
456 }
457 }
458
459 pub fn assign(&mut self, var: usize, val: bool) {
461 let mut frame = Frame {
462 var,
463 rows: Vec::new(),
464 pivots: Vec::new(),
465 touched_rows: std::collections::HashSet::new(),
466 touched_pivots: std::collections::HashSet::new(),
467 };
468 if let Some(r0) = self.pivot_row[var] {
469 Self::snap_pivot(&mut frame, &self.pivot_row, var);
471 Self::snap_row(&mut frame, &self.rows, r0);
472 self.rows[r0].rhs ^= val;
473 bit_clear(&mut self.rows[r0].bits, var);
474 self.weight[r0] -= 1;
475 self.note_weight(r0);
476 occ_remove(&mut self.occ[var], r0);
477 self.pivot_row[var] = None;
478 self.re_pivot(r0, &mut frame);
479 } else {
480 let rows_with_var = std::mem::take(&mut self.occ[var]);
484 for i in rows_with_var {
485 if bit_get(&self.rows[i].bits, var) {
486 Self::snap_row(&mut frame, &self.rows, i);
487 self.rows[i].rhs ^= val;
488 bit_clear(&mut self.rows[i].bits, var);
489 self.weight[i] -= 1;
490 self.note_weight(i);
491 }
492 }
493 }
494 self.value[var] = Some(val);
495 self.applied.push(var);
496 self.undo.push(frame);
497 }
498
499 fn xor_row(rows: &mut [IncRow], occ: &mut [Vec<usize>], weight: &mut [u32], j: usize, pivot: &IncRow) {
503 let mut delta: i64 = 0;
504 for b in set_bits(&pivot.bits) {
505 if bit_get(&rows[j].bits, b) {
506 occ_remove(&mut occ[b], j);
507 delta -= 1;
508 } else {
509 occ[b].push(j);
510 delta += 1;
511 }
512 }
513 xor_into(&mut rows[j].bits, &pivot.bits);
514 rows[j].rhs ^= pivot.rhs;
515 xor_into(&mut rows[j].prov, &pivot.prov);
516 weight[j] = (weight[j] as i64 + delta) as u32;
517 }
518
519 fn re_pivot(&mut self, r0: usize, frame: &mut Frame) {
523 let Some(q) = lowest_set(&self.rows[r0].bits) else {
524 return; };
526 let pivot = self.rows[r0].clone();
527 let targets = self.occ[q].clone();
528 for j in targets {
529 if j != r0 && bit_get(&self.rows[j].bits, q) {
530 Self::snap_row(frame, &self.rows, j);
531 Self::xor_row(&mut self.rows, &mut self.occ, &mut self.weight, j, &pivot);
532 self.note_weight(j);
533 }
534 }
535 Self::snap_pivot(frame, &self.pivot_row, q);
536 self.pivot_row[q] = Some(r0);
537 }
538
539 pub fn unassign(&mut self) {
543 let Some(frame) = self.undo.pop() else { return };
544 for (i, old) in frame.rows.into_iter().rev() {
545 for w in 0..self.var_words {
546 let mut diff = self.rows[i].bits[w] ^ old.bits[w];
547 while diff != 0 {
548 let b = w * 64 + diff.trailing_zeros() as usize;
549 diff &= diff - 1;
550 if bit_get(&old.bits, b) {
551 self.occ[b].push(i); } else {
553 occ_remove(&mut self.occ[b], i); }
555 }
556 }
557 self.weight[i] = popcount(&old.bits);
558 self.note_weight(i);
559 self.rows[i] = old;
560 }
561 for (v, old) in frame.pivots.into_iter().rev() {
562 self.pivot_row[v] = old;
563 }
564 self.value[frame.var] = None;
565 self.applied.pop();
566 }
567
568 fn derived(&self, prov: &[u64]) -> Vec<u64> {
569 let mut vars = vec![0u64; self.var_words];
570 for i in set_bits(prov) {
571 xor_into(&mut vars, &self.orig_vars[i]);
572 }
573 vars
574 }
575
576 fn gadget(&self, dv: &[u64], forced: Option<(usize, bool)>) -> Vec<Lit> {
577 let mut clause = Vec::new();
578 for u in set_bits(dv) {
579 match forced {
580 Some((fv, fval)) if fv == u => clause.push(Lit::new(u as u32, fval)),
581 _ => clause.push(Lit::new(u as u32, !self.value[u].expect("gadget var must be assigned"))),
582 }
583 }
584 clause
585 }
586
587 pub fn density(&self) -> (usize, f64, usize) {
591 let weights: Vec<usize> = self.rows.iter().map(|r| popcount(&r.bits) as usize).collect();
592 let n = weights.len().max(1);
593 let sum: usize = weights.iter().sum();
594 (weights.len(), sum as f64 / n as f64, weights.iter().copied().max().unwrap_or(0))
595 }
596
597 pub fn next_branch(&self) -> Option<usize> {
602 let mut fallback = None;
603 for v in 0..self.num_vars {
604 if self.value[v].is_some() || self.pivot_row[v].is_some() {
605 continue;
606 }
607 if self.rows.iter().any(|r| bit_get(&r.bits, v)) {
608 return Some(v);
609 }
610 if fallback.is_none() {
611 fallback = Some(v);
612 }
613 }
614 fallback
615 }
616
617 pub fn decision_vars(&self) -> Vec<usize> {
621 (0..self.num_vars).filter(|&v| self.pivot_row[v].is_none()).collect()
622 }
623
624 pub fn derived_clauses(&self, max_width: usize) -> Vec<Vec<Lit>> {
630 let mut out = Vec::new();
631 for row in &self.rows {
632 let vars = set_bits(&row.bits);
633 let k = vars.len();
634 if k == 0 || k > max_width || k > 31 {
635 continue;
636 }
637 for mask in 0u32..(1u32 << k) {
638 if ((mask.count_ones() % 2) == 1) != row.rhs {
639 out.push((0..k).map(|i| Lit::new(vars[i] as u32, (mask >> i) & 1 == 0)).collect());
640 }
641 }
642 }
643 out
644 }
645
646 pub fn rank(&self) -> usize {
648 (0..self.num_vars).filter(|&v| self.value[v].is_none() && self.pivot_row[v].is_some()).count()
649 }
650
651 pub fn kernel_dim(&self) -> usize {
654 (0..self.num_vars)
655 .filter(|&v| self.value[v].is_none() && self.pivot_row[v].is_none() && self.rows.iter().any(|r| bit_get(&r.bits, v)))
656 .count()
657 }
658
659 #[cfg(test)]
663 fn check_occ(&self) {
664 use std::collections::BTreeSet;
665 for v in 0..self.num_vars {
666 let mut seen = BTreeSet::new();
667 for &i in &self.occ[v] {
668 assert!(seen.insert(i), "occ[{v}] has duplicate row {i}");
669 assert!(bit_get(&self.rows[i].bits, v), "occ[{v}] lists row {i} which lacks {v}");
670 }
671 for (i, row) in self.rows.iter().enumerate() {
672 if bit_get(&row.bits, v) {
673 assert!(seen.contains(&i), "row {i} contains {v} but occ[{v}] omits it");
674 }
675 }
676 }
677 let in_low_set: std::collections::BTreeSet<usize> = self.low.iter().copied().collect();
680 for (r, row) in self.rows.iter().enumerate() {
681 assert_eq!(self.weight[r], popcount(&row.bits), "weight[{r}] desynced");
682 if self.weight[r] <= 1 {
683 assert!(self.in_low[r], "row {r} is weight ≤1 but in_low is false");
684 }
685 assert_eq!(self.in_low[r], in_low_set.contains(&r), "in_low[{r}] disagrees with low");
686 }
687 }
688
689 pub fn state(&mut self) -> Result<Vec<(Lit, Vec<Lit>)>, Vec<Lit>> {
693 let mut forced = Vec::new();
694 let candidates = std::mem::take(&mut self.low);
695 let mut keep = Vec::with_capacity(candidates.len());
696 let mut conflict: Option<Vec<Lit>> = None;
697 for r in candidates {
698 let w = self.weight[r];
699 if w > 1 {
700 self.in_low[r] = false; continue;
702 }
703 keep.push(r); if conflict.is_some() {
705 continue;
706 }
707 match w {
708 0 => {
709 if self.rows[r].rhs {
710 conflict = Some(self.gadget(&self.derived(&self.rows[r].prov), None));
711 }
712 }
713 _ => {
714 let v = lowest_set(&self.rows[r].bits).unwrap();
715 let rhs = self.rows[r].rhs;
716 forced.push((Lit::new(v as u32, rhs), self.gadget(&self.derived(&self.rows[r].prov), Some((v, rhs)))));
717 }
718 }
719 }
720 self.low = keep;
721 match conflict {
722 Some(c) => Err(c),
723 None => Ok(forced),
724 }
725 }
726}
727
728impl crate::cdcl::Theory for XorEngine {
732 fn propagate(&mut self, trail: &[Lit]) -> Vec<Vec<Lit>> {
733 let mut a: Vec<Option<bool>> = vec![None; self.num_vars];
734 for &l in trail {
735 a[l.var() as usize] = Some(l.is_positive());
736 }
737 match self.all_consequences(&a) {
738 Err(conflict) => vec![conflict],
739 Ok(forced) => forced.into_iter().map(|(_, reason)| reason).collect(),
740 }
741 }
742}
743
744impl crate::cdcl::Theory for IncXor {
748 fn propagate(&mut self, trail: &[Lit]) -> Vec<Vec<Lit>> {
749 let mut cp = 0;
751 while cp < self.applied.len() && cp < trail.len() && self.applied[cp] == trail[cp].var() as usize {
752 cp += 1;
753 }
754 while self.applied.len() > cp {
755 self.unassign();
756 }
757 let start = self.applied.len();
758 for &l in &trail[start..] {
759 let v = l.var() as usize;
760 if self.value[v].is_none() {
761 self.assign(v, l.is_positive());
762 }
763 }
764 match self.state() {
766 Err(conflict) => vec![conflict],
767 Ok(forced) => forced.into_iter().map(|(_, reason)| reason).collect(),
768 }
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775 use crate::xorsat::XorEquation;
776
777 fn oracle(num_vars: usize, eqs: &[XorEquation], assign: &[Option<bool>]) -> Option<Vec<Option<bool>>> {
782 let free: Vec<usize> = (0..num_vars).filter(|&v| assign[v].is_none()).collect();
783 let mut seen: Vec<Option<bool>> = vec![None; num_vars];
784 let mut first = true;
785 let mut any = false;
786 assert!(free.len() <= 16, "oracle only for small systems");
787 for mask in 0u32..(1u32 << free.len()) {
788 let mut full = vec![false; num_vars];
789 for v in 0..num_vars {
790 full[v] = assign[v].unwrap_or(false);
791 }
792 for (i, &v) in free.iter().enumerate() {
793 full[v] = (mask >> i) & 1 == 1;
794 }
795 let ok = eqs.iter().all(|e| e.vars.iter().fold(false, |a, &v| a ^ full[v]) == e.rhs);
796 if !ok {
797 continue;
798 }
799 any = true;
800 if first {
801 for v in 0..num_vars {
802 seen[v] = Some(full[v]);
803 }
804 first = false;
805 } else {
806 for v in 0..num_vars {
807 if seen[v] != Some(full[v]) {
808 seen[v] = None; }
810 }
811 }
812 }
813 if any {
814 for v in 0..num_vars {
816 if assign[v].is_some() {
817 seen[v] = None;
818 }
819 }
820 Some(seen)
821 } else {
822 None
823 }
824 }
825
826 fn lit_sat(clause: &[Lit], full: &[bool]) -> bool {
827 clause.iter().any(|l| full[l.var() as usize] == l.is_positive())
828 }
829
830 #[test]
831 fn forces_a_variable_by_combining_two_equations() {
832 let eqs = vec![XorEquation::new(vec![0, 1], true), XorEquation::new(vec![0, 2], true)];
834 let eng = XorEngine::new(3, &eqs);
835 let assign = vec![None, Some(true), None];
836 let forced = eng.all_consequences(&assign).expect("system is consistent");
837 let (x2, reason) = forced.iter().find(|(l, _)| l.var() == 2).expect("x2 must be forced");
838 assert!(x2.is_positive(), "x2 must be forced true");
839 let unassigned: Vec<_> = reason.iter().filter(|l| assign[l.var() as usize].is_none()).collect();
841 assert_eq!(unassigned.len(), 1);
842 assert_eq!(unassigned[0].var(), 2);
843 }
844
845 #[test]
846 fn dpll_xor_decides_a_small_mixed_instance_via_the_theory() {
847 use crate::cdcl::{SolveResult, Solver};
848 let gadget = vec![
850 vec![Lit::new(0, true), Lit::new(1, true), Lit::new(2, false)],
851 vec![Lit::new(0, true), Lit::new(1, false), Lit::new(2, true)],
852 vec![Lit::new(0, false), Lit::new(1, true), Lit::new(2, true)],
853 vec![Lit::new(0, false), Lit::new(1, false), Lit::new(2, false)],
854 vec![Lit::new(0, true)],
855 vec![Lit::new(1, true)],
856 ];
857 let mut solver = Solver::new(3);
858 for c in &gadget {
859 solver.add_clause(c.clone());
860 }
861 let engine = XorEngine::new(3, &[XorEquation::new(vec![0, 1, 2], false)]);
862 let mut theories: Vec<Box<dyn crate::cdcl::Theory>> = vec![Box::new(engine)];
863 match solver.solve_with(&mut theories) {
864 SolveResult::Sat(m) => {
865 for c in &gadget {
866 assert!(c.iter().any(|l| m[l.var() as usize] == l.is_positive()), "model fails {c:?}");
867 }
868 assert!(m[0] && m[1] && !m[2], "expected x0,x1 true and x2 false");
869 }
870 SolveResult::Unsat => panic!("instance is SAT"),
871 }
872 }
873
874 #[test]
875 fn live_dpll_xor_matches_plain_cdcl_on_random_xor_formulas() {
876 use crate::cdcl::{SolveResult, Solver};
877 let mut rng: u64 = 0x2545_F491_4F6C_DD1D;
883 let mut next = || {
884 rng ^= rng << 13;
885 rng ^= rng >> 7;
886 rng ^= rng << 17;
887 rng
888 };
889 let gadget_of = |e: &XorEquation, _nv: usize| -> Vec<Vec<Lit>> {
891 let vs = &e.vars;
892 let k = vs.len();
893 let mut out = Vec::new();
894 for mask in 0u32..(1u32 << k) {
895 if ((mask.count_ones() % 2) == 1) != e.rhs {
896 out.push((0..k).map(|i| Lit::new(vs[i] as u32, (mask >> i) & 1 == 0)).collect());
897 }
898 }
899 out
900 };
901 for _ in 0..400 {
902 let num_vars = 4 + (next() % 7) as usize; let n_eqs = 1 + (next() % 4) as usize;
904 let mut eqs = Vec::new();
905 let mut clauses: Vec<Vec<Lit>> = Vec::new();
906 for _ in 0..n_eqs {
907 let mut vars: Vec<usize> = (0..num_vars).filter(|_| next() % 2 == 0).collect();
908 vars.truncate(4); if vars.is_empty() {
910 continue;
911 }
912 let e = XorEquation::new(vars, next() % 2 == 0);
913 clauses.extend(gadget_of(&e, num_vars));
914 eqs.push(e);
915 }
916 if eqs.is_empty() {
917 continue;
918 }
919 for _ in 0..(2 + next() % 4) {
921 let w = 1 + (next() % 2) as usize;
922 let c: Vec<Lit> = (0..w)
923 .map(|_| Lit::new((next() % num_vars as u64) as u32, next() % 2 == 0))
924 .collect();
925 clauses.push(c);
926 }
927
928 let mut plain = Solver::new(num_vars);
929 for c in &clauses {
930 plain.add_clause(c.clone());
931 }
932 let truth = plain.solve();
933
934 let mut live = Solver::new(num_vars);
935 for c in &clauses {
936 live.add_clause(c.clone());
937 }
938 let engine = IncXor::new(num_vars, &eqs);
939 let mut theories: Vec<Box<dyn crate::cdcl::Theory>> = vec![Box::new(engine)];
940 let got = live.solve_with(&mut theories);
941
942 match (&truth, &got) {
943 (SolveResult::Sat(_), SolveResult::Sat(m)) => {
944 assert!(
945 clauses.iter().all(|c| c.iter().any(|l| m[l.var() as usize] == l.is_positive())),
946 "live model fails a clause (eqs={eqs:?})"
947 );
948 }
949 (SolveResult::Unsat, SolveResult::Unsat) => {}
950 _ => panic!("verdict mismatch: plain={truth:?} live={got:?} (eqs={eqs:?} clauses={clauses:?})"),
951 }
952 }
953 }
954
955 #[test]
956 #[ignore = "diagnostic: reads par32 from disk and prints the GF(2) search dimensions"]
957 fn par32_search_dimensions() {
958 for name in ["par16-1-c", "par32-1-c", "par32-1"] {
959 let path = format!("../../benchmarks/arena/instances/sat/crafted_parity/{name}.cnf");
960 let Ok(text) = std::fs::read_to_string(&path) else {
961 eprintln!("{name}: missing");
962 continue;
963 };
964 let cnf = crate::dimacs::parse(&text).expect("valid dimacs");
965 let eqs = crate::lyapunov::extract_xor(cnf.num_vars, &cnf.clauses);
966 let engine = IncXor::new(cnf.num_vars, &eqs);
967 let (rows, avgw, maxw) = engine.density();
968 eprintln!(
969 "{name}: vars={} clauses={} xor_eqs={} | rank={} kernel_dim={} decision_vars={} | rows={} avg_w={:.1} max_w={}",
970 cnf.num_vars,
971 cnf.clauses.len(),
972 eqs.len(),
973 engine.rank(),
974 engine.kernel_dim(),
975 engine.decision_vars().len(),
976 rows,
977 avgw,
978 maxw,
979 );
980 }
981 }
982
983 #[test]
984 #[ignore = "diagnostic: measures how far probing collapses par32's GF(2) kernel"]
985 fn par32_probing_collapses_kernel() {
986 use crate::xorsat::XorEquation;
987 fn probe(num_vars: usize, clauses: &[Vec<Lit>], eqs: &[XorEquation], start: Lit) -> Option<Vec<Option<bool>>> {
990 let mut val: Vec<Option<bool>> = vec![None; num_vars];
991 let mut in_engine = vec![false; num_vars];
992 let mut engine = IncXor::new(num_vars, eqs);
993 val[start.var() as usize] = Some(start.is_positive());
994 let mut progress = true;
995 while progress {
996 progress = false;
997 for v in 0..num_vars {
998 if val[v].is_some() && !in_engine[v] {
999 engine.assign(v, val[v].unwrap());
1000 in_engine[v] = true;
1001 }
1002 }
1003 match engine.state() {
1004 Err(_) => return None,
1005 Ok(forced) => {
1006 for (lit, _) in forced {
1007 let v = lit.var() as usize;
1008 match val[v] {
1009 None => {
1010 val[v] = Some(lit.is_positive());
1011 progress = true;
1012 }
1013 Some(b) => {
1014 if b != lit.is_positive() {
1015 return None;
1016 }
1017 }
1018 }
1019 }
1020 }
1021 }
1022 for c in clauses {
1023 let mut sat = false;
1024 let mut unset: Option<Lit> = None;
1025 let mut n_unset = 0;
1026 for &l in c {
1027 match val[l.var() as usize] {
1028 Some(b) if b == l.is_positive() => {
1029 sat = true;
1030 break;
1031 }
1032 Some(_) => {}
1033 None => {
1034 n_unset += 1;
1035 unset = Some(l);
1036 }
1037 }
1038 }
1039 if sat {
1040 continue;
1041 }
1042 if n_unset == 0 {
1043 return None;
1044 }
1045 if n_unset == 1 {
1046 let l = unset.unwrap();
1047 if val[l.var() as usize].is_none() {
1048 val[l.var() as usize] = Some(l.is_positive());
1049 progress = true;
1050 }
1051 }
1052 }
1053 }
1054 Some(val)
1055 }
1056
1057 for name in ["par16-1-c", "par32-1-c"] {
1058 let path = format!("../../benchmarks/arena/instances/sat/crafted_parity/{name}.cnf");
1059 let Ok(text) = std::fs::read_to_string(&path) else {
1060 eprintln!("{name}: missing");
1061 continue;
1062 };
1063 let cnf = crate::dimacs::parse(&text).expect("valid dimacs");
1064 let eqs = crate::lyapunov::extract_xor(cnf.num_vars, &cnf.clauses);
1065 let base = IncXor::new(cnf.num_vars, &eqs);
1066 let kernel = base.decision_vars();
1067 let kernel0 = base.kernel_dim();
1068
1069 let mut folded = eqs.clone();
1070 let (mut failed, mut backbone, mut equiv) = (0usize, 0usize, 0usize);
1071 for &x in &kernel {
1072 let f1 = probe(cnf.num_vars, &cnf.clauses, &eqs, Lit::new(x as u32, true));
1073 let f0 = probe(cnf.num_vars, &cnf.clauses, &eqs, Lit::new(x as u32, false));
1074 match (&f1, &f0) {
1075 (None, None) => {} (None, Some(_)) => {
1077 failed += 1;
1078 folded.push(XorEquation::new(vec![x], false));
1079 }
1080 (Some(_), None) => {
1081 failed += 1;
1082 folded.push(XorEquation::new(vec![x], true));
1083 }
1084 (Some(a1), Some(a0)) => {
1085 for y in 0..cnf.num_vars {
1086 if y == x {
1087 continue;
1088 }
1089 if let (Some(v1), Some(v0)) = (a1[y], a0[y]) {
1090 if v1 == v0 {
1091 backbone += 1;
1092 folded.push(XorEquation::new(vec![y], v1));
1093 } else {
1094 equiv += 1;
1095 folded.push(XorEquation::new(vec![x, y], v0)); }
1097 }
1098 }
1099 }
1100 }
1101 }
1102 let after = IncXor::new(cnf.num_vars, &folded);
1103 eprintln!(
1104 "{name}: kernel {} → {} | probes found failed_lits={} backbone_hits={} equiv_pairs={} (raw counts, pre-dedup)",
1105 kernel0,
1106 after.kernel_dim(),
1107 failed,
1108 backbone,
1109 equiv,
1110 );
1111 }
1112 }
1113
1114 #[test]
1115 fn detects_a_linear_contradiction() {
1116 let eqs = vec![
1119 XorEquation::new(vec![0, 1], false),
1120 XorEquation::new(vec![1, 2], false),
1121 XorEquation::new(vec![0, 2], true),
1122 ];
1123 let eng = XorEngine::new(3, &eqs);
1124 match eng.analyze(&vec![None, None, None]) {
1125 XorStep::Conflict(_) => {}
1126 other => panic!("expected conflict, got {other:?}"),
1127 }
1128 }
1129
1130 #[test]
1131 fn next_branch_picks_a_free_kernel_var_and_breaking_it_collapses_the_rest() {
1132 let eqs = vec![XorEquation::new(vec![0, 1], false), XorEquation::new(vec![2, 3], false)];
1135 let mut inc = IncXor::new(4, &eqs);
1136 assert_eq!(inc.rank(), 2);
1137 assert_eq!(inc.kernel_dim(), 2);
1138 let b = inc.next_branch().expect("a free kernel var to break on");
1139 assert!(b == 1 || b == 3, "break point must be a non-pivot free var, got {b}");
1140 inc.assign(b, true);
1141 let forced = inc.state().expect("consistent");
1142 assert!(!forced.is_empty(), "breaking a kernel var must force its pivot partner");
1143 }
1144
1145 #[test]
1146 fn incremental_engine_matches_the_recompute_oracle_under_random_trails() {
1147 let mut rng: u64 = 0xD1B5_4A32_D192_ED03;
1150 let mut next = || {
1151 rng ^= rng << 13;
1152 rng ^= rng >> 7;
1153 rng ^= rng << 17;
1154 rng
1155 };
1156 let forced_set = |r: &Result<Vec<(Lit, Vec<Lit>)>, Vec<Lit>>| -> Option<std::collections::BTreeSet<(u32, bool)>> {
1157 match r {
1158 Err(_) => None, Ok(f) => Some(f.iter().map(|(l, _)| (l.var(), l.is_positive())).collect()),
1160 }
1161 };
1162 for _ in 0..2000 {
1163 let num_vars = 3 + (next() % 6) as usize; let n_eqs = 1 + (next() % 6) as usize;
1165 let mut eqs = Vec::new();
1166 for _ in 0..n_eqs {
1167 let vars: Vec<usize> = (0..num_vars).filter(|_| next() % 2 == 0).collect();
1168 if !vars.is_empty() {
1169 eqs.push(XorEquation::new(vars, next() % 2 == 0));
1170 }
1171 }
1172 if eqs.is_empty() {
1173 continue;
1174 }
1175 let oracle = XorEngine::new(num_vars, &eqs);
1176 let mut inc = IncXor::new(num_vars, &eqs);
1177 let mut assign: Vec<Option<bool>> = vec![None; num_vars];
1178 let mut stack: Vec<usize> = Vec::new();
1179
1180 for _step in 0..40 {
1181 let free: Vec<usize> = (0..num_vars).filter(|&v| assign[v].is_none()).collect();
1182 let push = !free.is_empty() && (stack.is_empty() || next() % 2 == 0);
1183 if push {
1184 let v = free[(next() % free.len() as u64) as usize];
1185 let val = next() % 2 == 0;
1186 inc.assign(v, val);
1187 assign[v] = Some(val);
1188 stack.push(v);
1189 } else if let Some(v) = stack.pop() {
1190 inc.unassign();
1191 assign[v] = None;
1192 }
1193 inc.check_occ();
1194
1195 let got = inc.state();
1196 let truth = oracle.all_consequences(&assign);
1197 assert_eq!(got.is_err(), truth.is_err(),
1199 "conflict mismatch: inc={:?} oracle_err={} (eqs={eqs:?} assign={assign:?})",
1200 got.is_err(), truth.is_err());
1201 if got.is_ok() {
1202 assert_eq!(forced_set(&got), forced_set(&truth),
1203 "forced-set mismatch (eqs={eqs:?} assign={assign:?})");
1204 if let Ok(f) = &got {
1206 for (lit, reason) in f {
1207 let un: Vec<_> = reason.iter().filter(|l| assign[l.var() as usize].is_none()).collect();
1208 assert_eq!(un.len(), 1, "reason must be unit");
1209 assert_eq!(un[0].var(), lit.var());
1210 }
1211 }
1212 }
1213 }
1214 }
1215 }
1216
1217 #[test]
1218 fn matches_the_brute_force_oracle_exhaustively() {
1219 let mut rng: u64 = 0x9E3779B97F4A7C15;
1223 let mut next = || {
1224 rng ^= rng << 13;
1225 rng ^= rng >> 7;
1226 rng ^= rng << 17;
1227 rng
1228 };
1229 for _ in 0..3000 {
1230 let num_vars = 3 + (next() % 5) as usize; let n_eqs = 1 + (next() % 6) as usize;
1232 let mut eqs = Vec::new();
1233 for _ in 0..n_eqs {
1234 let vars: Vec<usize> = (0..num_vars).filter(|_| next() % 2 == 0).collect();
1235 if vars.is_empty() {
1236 continue;
1237 }
1238 eqs.push(XorEquation::new(vars, next() % 2 == 0));
1239 }
1240 if eqs.is_empty() {
1241 continue;
1242 }
1243 let assign: Vec<Option<bool>> =
1245 (0..num_vars).map(|_| match next() % 3 { 0 => Some(true), 1 => Some(false), _ => None }).collect();
1246
1247 let eng = XorEngine::new(num_vars, &eqs);
1248 let got = eng.all_consequences(&assign);
1249 let truth = oracle(num_vars, &eqs, &assign);
1250
1251 match (got, truth) {
1252 (Err(conflict), None) => {
1253 for l in &conflict {
1255 assert_eq!(assign[l.var() as usize], Some(!l.is_positive()),
1256 "conflict literal must be false under the assignment");
1257 }
1258 for mask in 0u32..(1u32 << num_vars) {
1260 let full: Vec<bool> = (0..num_vars).map(|v| (mask >> v) & 1 == 1).collect();
1261 if eqs.iter().all(|e| e.vars.iter().fold(false, |a, &x| a ^ full[x]) == e.rhs) {
1262 assert!(lit_sat(&conflict, &full), "conflict clause must be implied");
1263 }
1264 }
1265 }
1266 (Ok(forced), Some(truth_forced)) => {
1267 for (lit, reason) in &forced {
1269 let v = lit.var() as usize;
1270 assert_eq!(truth_forced[v], Some(lit.is_positive()),
1271 "engine forced x{v}={} but oracle disagrees (eqs={eqs:?}, assign={assign:?})", lit.is_positive());
1272 let un: Vec<_> = reason.iter().filter(|l| assign[l.var() as usize].is_none()).collect();
1274 assert_eq!(un.len(), 1, "reason must be unit");
1275 assert_eq!(un[0].var(), lit.var());
1276 for mask in 0u32..(1u32 << num_vars) {
1278 let full: Vec<bool> = (0..num_vars).map(|v| (mask >> v) & 1 == 1).collect();
1279 let sat_all = eqs.iter().all(|e| e.vars.iter().fold(false, |a, &x| a ^ full[x]) == e.rhs);
1280 if sat_all {
1281 assert!(lit_sat(reason, &full), "reason clause must hold in every solution");
1282 }
1283 }
1284 }
1285 let engine_vars: std::collections::HashSet<usize> =
1287 forced.iter().map(|(l, _)| l.var() as usize).collect();
1288 for v in 0..num_vars {
1289 if assign[v].is_none() && truth_forced[v].is_some() {
1290 assert!(engine_vars.contains(&v),
1291 "engine missed forced x{v} (eqs={eqs:?}, assign={assign:?})");
1292 }
1293 }
1294 }
1295 (Ok(forced), None) => panic!("engine missed a contradiction: forced={forced:?} eqs={eqs:?} assign={assign:?}"),
1296 (Err(c), Some(_)) => panic!("engine reported a false conflict {c:?}: eqs={eqs:?} assign={assign:?}"),
1297 }
1298 }
1299 }
1300}