1#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct XorEquation {
17 pub vars: Vec<usize>,
19 pub rhs: bool,
21}
22
23impl XorEquation {
24 pub fn new(vars: impl Into<Vec<usize>>, rhs: bool) -> Self {
26 XorEquation { vars: vars.into(), rhs }
27 }
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
32pub enum XorOutcome {
33 Sat(Vec<bool>),
35 Unsat(Vec<usize>),
38}
39
40#[inline]
41fn get(bits: &[u64], i: usize) -> bool {
42 (bits[i / 64] >> (i % 64)) & 1 == 1
43}
44
45#[inline]
46fn flip(bits: &mut [u64], i: usize) {
47 bits[i / 64] ^= 1u64 << (i % 64);
48}
49
50#[inline]
51fn xor_assign(dst: &mut [u64], src: &[u64]) {
52 for (d, s) in dst.iter_mut().zip(src) {
53 *d ^= *s;
54 }
55}
56
57#[inline]
58fn is_zero(bits: &[u64]) -> bool {
59 bits.iter().all(|&w| w == 0)
60}
61
62fn set_indices(bits: &[u64]) -> Vec<usize> {
63 let mut out = Vec::new();
64 for (w, &word) in bits.iter().enumerate() {
65 let mut b = word;
66 while b != 0 {
67 let t = b.trailing_zeros() as usize;
68 out.push(w * 64 + t);
69 b &= b - 1;
70 }
71 }
72 out
73}
74
75#[derive(Clone)]
76struct Row {
77 lhs: Vec<u64>, rhs: bool,
79 prov: Vec<u64>, }
81
82pub fn solve(equations: &[XorEquation], num_vars: usize) -> XorOutcome {
85 let nb = num_vars.div_ceil(64).max(1);
86 let pb = equations.len().div_ceil(64).max(1);
87 let mut rows: Vec<Row> = equations
88 .iter()
89 .enumerate()
90 .map(|(i, eq)| {
91 let mut lhs = vec![0u64; nb];
92 for &v in &eq.vars {
93 if v < num_vars {
94 flip(&mut lhs, v); }
96 }
97 let mut prov = vec![0u64; pb];
98 flip(&mut prov, i);
99 Row { lhs, rhs: eq.rhs, prov }
100 })
101 .collect();
102
103 let mut pivot_for_col = vec![usize::MAX; num_vars];
104 let mut rank = 0;
105 for c in 0..num_vars {
106 let Some(p) = (rank..rows.len()).find(|&i| get(&rows[i].lhs, c)) else {
107 continue;
108 };
109 rows.swap(rank, p);
110 let pivot = rows[rank].clone();
111 for (i, row) in rows.iter_mut().enumerate() {
112 if i != rank && get(&row.lhs, c) {
113 xor_assign(&mut row.lhs, &pivot.lhs);
114 row.rhs ^= pivot.rhs;
115 xor_assign(&mut row.prov, &pivot.prov);
116 }
117 }
118 pivot_for_col[c] = rank;
119 rank += 1;
120 }
121
122 for row in &rows {
124 if is_zero(&row.lhs) && row.rhs {
125 return XorOutcome::Unsat(set_indices(&row.prov));
126 }
127 }
128
129 let mut assignment = vec![false; num_vars];
132 for c in 0..num_vars {
133 let pr = pivot_for_col[c];
134 if pr != usize::MAX {
135 assignment[c] = rows[pr].rhs;
136 }
137 }
138 XorOutcome::Sat(assignment)
139}
140
141pub fn satisfies(equations: &[XorEquation], assignment: &[bool]) -> bool {
143 equations.iter().all(|eq| {
144 let ones = eq
145 .vars
146 .iter()
147 .filter(|&&v| v < assignment.len() && assignment[v])
148 .count();
149 (ones % 2 == 1) == eq.rhs
150 })
151}
152
153pub fn is_refutation(equations: &[XorEquation], num_vars: usize, refutation: &[usize]) -> bool {
156 if refutation.is_empty() {
157 return false;
158 }
159 let nb = num_vars.div_ceil(64).max(1);
160 let mut lhs = vec![0u64; nb];
161 let mut rhs = false;
162 for &idx in refutation {
163 let Some(eq) = equations.get(idx) else {
164 return false;
165 };
166 for &v in &eq.vars {
167 if v < num_vars {
168 flip(&mut lhs, v);
169 }
170 }
171 rhs ^= eq.rhs;
172 }
173 is_zero(&lhs) && rhs
174}
175
176pub fn refute_via_parity(e: &crate::ProofExpr) -> bool {
188 use std::collections::HashMap;
189 let mut idx: HashMap<String, usize> = HashMap::new();
190 let mut clauses: Vec<Vec<(usize, bool)>> = Vec::new();
191 if !collect_clauses(e, &mut clauses, &mut idx) {
192 return false;
193 }
194 let mut groups: HashMap<Vec<usize>, Vec<Vec<(usize, bool)>>> = HashMap::new();
196 for c in clauses {
197 let mut vars: Vec<usize> = c.iter().map(|&(v, _)| v).collect();
198 vars.sort_unstable();
199 vars.dedup();
200 if vars.len() != c.len() {
201 continue; }
203 groups.entry(vars).or_default().push(c);
204 }
205 let num_vars = idx.len();
206 let mut eqs = Vec::new();
207 for (vars, group) in groups {
208 let k = vars.len();
209 if k == 0 || k > 31 || group.len() != (1usize << (k - 1)) {
210 continue; }
212 let pos: HashMap<usize, usize> = vars.iter().enumerate().map(|(i, &v)| (v, i)).collect();
213 let mut forbidden = std::collections::HashSet::new();
214 let mut parity: Option<u32> = None;
215 let mut clean = true;
216 for c in &group {
217 let mut a = 0u32;
219 for &(v, positive) in c {
220 if !positive {
221 a |= 1 << pos[&v];
222 }
223 }
224 if !forbidden.insert(a) {
225 clean = false; break;
227 }
228 let p = a.count_ones() % 2;
229 match parity {
230 None => parity = Some(p),
231 Some(q) if q != p => {
232 clean = false; break;
234 }
235 _ => {}
236 }
237 }
238 if !clean {
239 continue;
240 }
241 let p = parity.unwrap_or(0);
243 eqs.push(XorEquation::new(vars, p == 0));
244 }
245 if eqs.is_empty() {
246 return false;
247 }
248 matches!(solve(&eqs, num_vars), XorOutcome::Unsat(_))
249}
250
251fn collect_clauses(
255 e: &crate::ProofExpr,
256 out: &mut Vec<Vec<(usize, bool)>>,
257 idx: &mut std::collections::HashMap<String, usize>,
258) -> bool {
259 use crate::ProofExpr;
260 match e {
261 ProofExpr::And(l, r) => collect_clauses(l, out, idx) && collect_clauses(r, out, idx),
262 other => {
263 let mut lits = Vec::new();
264 if collect_literals(other, true, &mut lits, idx) {
265 out.push(lits);
266 true
267 } else {
268 false
269 }
270 }
271 }
272}
273
274fn collect_literals(
275 e: &crate::ProofExpr,
276 positive: bool,
277 out: &mut Vec<(usize, bool)>,
278 idx: &mut std::collections::HashMap<String, usize>,
279) -> bool {
280 use crate::ProofExpr;
281 match e {
282 ProofExpr::Atom(name) => {
283 let n = idx.len();
284 let v = *idx.entry(name.clone()).or_insert(n);
285 out.push((v, positive));
286 true
287 }
288 ProofExpr::Not(inner) => match inner.as_ref() {
289 ProofExpr::Atom(name) => {
290 let n = idx.len();
291 let v = *idx.entry(name.clone()).or_insert(n);
292 out.push((v, !positive));
293 true
294 }
295 _ => false,
296 },
297 ProofExpr::Or(l, r) if positive => {
298 collect_literals(l, positive, out, idx) && collect_literals(r, positive, out, idx)
299 }
300 _ => false,
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 fn eq(vars: &[usize], rhs: bool) -> XorEquation {
309 XorEquation::new(vars.to_vec(), rhs)
310 }
311
312 fn xor_system_to_expr(system: &[(Vec<usize>, bool)]) -> crate::ProofExpr {
315 use crate::ProofExpr;
316 let lit = |v: usize, positive: bool| {
317 let a = ProofExpr::Atom(format!("x{v}"));
318 if positive { a } else { ProofExpr::Not(Box::new(a)) }
319 };
320 let mut clauses = Vec::new();
321 for (vars, rhs) in system {
322 let k = vars.len();
323 for mask in 0u32..(1 << k) {
324 let parity = mask.count_ones() % 2 == 1;
325 if parity != *rhs {
326 let mut it = vars.iter().enumerate();
327 let (i0, &v0) = it.next().unwrap();
328 let first = lit(v0, mask & (1 << i0) == 0);
329 let clause = it.fold(first, |acc, (i, &v)| {
330 ProofExpr::Or(Box::new(acc), Box::new(lit(v, mask & (1 << i) == 0)))
331 });
332 clauses.push(clause);
333 }
334 }
335 }
336 let mut it = clauses.into_iter();
337 let first = it.next().expect("non-empty system");
338 it.fold(first, |acc, c| crate::ProofExpr::And(Box::new(acc), Box::new(c)))
339 }
340
341 #[test]
345 fn parity_shadow_refutes_inconsistent_xor_through_prove_unsat() {
346 let system = vec![
348 (vec![0, 1], true),
349 (vec![1, 2], true),
350 (vec![0, 2], true),
351 ];
352 let e = xor_system_to_expr(&system);
353 assert!(refute_via_parity(&e), "the parity shadow must recognize and refute the odd cycle");
354 assert_eq!(
355 crate::sat::prove_unsat(&e),
356 crate::sat::UnsatOutcome::Refuted,
357 "prove_unsat now routes the parity cover to Gaussian elimination"
358 );
359 }
360
361 #[test]
364 fn parity_shadow_is_sound_on_satisfiable_xor() {
365 let system = vec![(vec![0, 1], true), (vec![1, 2], true)];
367 let e = xor_system_to_expr(&system);
368 assert!(!refute_via_parity(&e), "a satisfiable parity system must not be refuted");
369 assert!(
370 matches!(crate::sat::prove_unsat(&e), crate::sat::UnsatOutcome::Sat(_)),
371 "prove_unsat must find a model for the satisfiable parity cover"
372 );
373 }
374
375 #[test]
378 fn parity_shadow_declines_non_parity_pigeonhole() {
379 use crate::cdcl::Lit;
380 use crate::ProofExpr;
381 let (cnf, _) = crate::families::php(4);
382 let clause_expr = |c: &[Lit]| {
383 let mut it = c.iter().map(|l| {
384 let a = ProofExpr::Atom(format!("x{}", l.var()));
385 if l.is_positive() { a } else { ProofExpr::Not(Box::new(a)) }
386 });
387 let first = it.next().expect("non-empty clause");
388 it.fold(first, |acc, l| ProofExpr::Or(Box::new(acc), Box::new(l)))
389 };
390 let mut it = cnf.clauses.iter().map(|c| clause_expr(c));
391 let first = it.next().unwrap();
392 let e = it.fold(first, |acc, c| ProofExpr::And(Box::new(acc), Box::new(c)));
393 assert!(!refute_via_parity(&e), "pigeonhole is not a parity cover — the GF(2) shadow declines");
394 }
395
396 #[test]
397 fn simple_consistent_system_is_solved() {
398 let sys = vec![eq(&[0, 1], true), eq(&[1], true)];
400 match solve(&sys, 2) {
401 XorOutcome::Sat(a) => {
402 assert!(satisfies(&sys, &a), "assignment must satisfy: {a:?}");
403 assert_eq!(a, vec![false, true]);
404 }
405 o => panic!("expected Sat, got {o:?}"),
406 }
407 }
408
409 #[test]
410 fn direct_contradiction_is_refuted() {
411 let sys = vec![eq(&[0, 1], false), eq(&[0, 1], true)];
413 match solve(&sys, 2) {
414 XorOutcome::Unsat(r) => {
415 assert!(is_refutation(&sys, 2, &r), "refutation must re-check: {r:?}");
416 assert_eq!(r.len(), 2, "both equations are needed");
417 }
418 o => panic!("expected Unsat, got {o:?}"),
419 }
420 }
421
422 #[test]
423 fn parity_chain_summing_to_one_is_refuted() {
424 let n = 8;
426 let mut sys: Vec<XorEquation> = (0..n - 1).map(|i| eq(&[i, i + 1], false)).collect();
427 sys.push(eq(&[0, n - 1], true));
428 match solve(&sys, n) {
429 XorOutcome::Unsat(r) => assert!(is_refutation(&sys, n, &r), "refutation invalid: {r:?}"),
430 o => panic!("inconsistent chain must be Unsat, got {o:?}"),
431 }
432 }
433
434 #[test]
435 fn duplicate_variables_cancel() {
436 let sys = vec![eq(&[0, 0, 1], true)];
438 match solve(&sys, 2) {
439 XorOutcome::Sat(a) => assert!(a[1], "x1 must be true: {a:?}"),
440 o => panic!("expected Sat, got {o:?}"),
441 }
442 }
443
444 #[test]
445 fn empty_system_is_trivially_sat() {
446 assert!(matches!(solve(&[], 3), XorOutcome::Sat(_)));
447 }
448
449 #[test]
450 fn matches_brute_force_on_random_systems() {
451 let mut s: u64 = 0xD1B54A32D192ED03;
454 let mut next = || {
455 s ^= s << 13;
456 s ^= s >> 7;
457 s ^= s << 17;
458 s
459 };
460 for _ in 0..400 {
461 let num_vars = (next() % 6) as usize + 1; let m = (next() % 8) as usize + 1; let sys: Vec<XorEquation> = (0..m)
464 .map(|_| {
465 let vars: Vec<usize> =
466 (0..num_vars).filter(|_| next() % 2 == 0).collect();
467 eq(&vars, next() % 2 == 0)
468 })
469 .collect();
470 let brute_sat = (0..(1u32 << num_vars)).any(|mask| {
471 let a: Vec<bool> = (0..num_vars).map(|i| (mask >> i) & 1 == 1).collect();
472 satisfies(&sys, &a)
473 });
474 match solve(&sys, num_vars) {
475 XorOutcome::Sat(a) => {
476 assert!(brute_sat, "we said SAT but brute force says UNSAT: {sys:?}");
477 assert!(satisfies(&sys, &a), "returned assignment is wrong: {a:?}");
478 }
479 XorOutcome::Unsat(r) => {
480 assert!(!brute_sat, "we said UNSAT but brute force found a model: {sys:?}");
481 assert!(is_refutation(&sys, num_vars, &r), "bogus refutation {r:?}");
482 }
483 }
484 }
485 }
486
487 #[test]
488 fn a_bad_refutation_is_rejected() {
489 let sys = vec![eq(&[0, 1], false), eq(&[0, 1], true)];
490 assert!(!is_refutation(&sys, 2, &[]), "empty is not a refutation");
491 assert!(!is_refutation(&sys, 2, &[0]), "one consistent equation is not 0=1");
492 assert!(is_refutation(&sys, 2, &[0, 1]), "the pair sums to 0=1");
493 }
494}