1use core::fmt::Write as _;
30
31use crate::cdcl::Lit;
32use crate::proof::{ProofStep, Witness};
33use crate::rup::{lit_val, set_true};
34
35#[derive(Clone, Debug, PartialEq, Eq)]
37pub enum EmitError {
38 StepNotRup { index: usize },
41 EmptyClauseNotRup,
43 RequiresSubstitutionRedundancy { index: usize },
46 PrInRupOnlyFormat { index: usize },
48 SizeCapExceeded,
52}
53
54impl From<core::fmt::Error> for EmitError {
55 fn from(_: core::fmt::Error) -> Self {
58 EmitError::SizeCapExceeded
59 }
60}
61
62pub struct SizeSink {
69 bytes: u64,
70 cap: u64,
71 overflowed: bool,
72}
73
74impl SizeSink {
75 pub fn new(cap: u64) -> Self {
77 Self { bytes: 0, cap, overflowed: false }
78 }
79 pub fn bytes(&self) -> u64 {
81 self.bytes
82 }
83 pub fn overflowed(&self) -> bool {
85 self.overflowed
86 }
87}
88
89impl core::fmt::Write for SizeSink {
90 fn write_str(&mut self, s: &str) -> core::fmt::Result {
91 self.bytes = self.bytes.saturating_add(s.len() as u64);
92 if self.bytes > self.cap {
93 self.overflowed = true;
94 return Err(core::fmt::Error);
95 }
96 Ok(())
97 }
98}
99
100#[inline]
102fn dimacs(l: Lit) -> i64 {
103 let v = (l.var() + 1) as i64;
104 if l.is_positive() {
105 v
106 } else {
107 -v
108 }
109}
110
111fn push_clause(out: &mut String, lits: &[Lit]) {
113 for &l in lits {
114 out.push_str(&dimacs(l).to_string());
115 out.push(' ');
116 }
117 out.push_str("0");
118}
119
120fn wpush_clause<W: core::fmt::Write>(w: &mut W, lits: &[Lit]) -> Result<(), EmitError> {
124 for &l in lits {
125 write!(w, "{} ", dimacs(l))?;
126 }
127 w.write_str("0")?;
128 Ok(())
129}
130
131fn wput<W: core::fmt::Write>(w: &mut W, l: Lit) -> Result<(), EmitError> {
134 write!(w, "{} ", dimacs(l))?;
135 Ok(())
136}
137
138fn canon(c: &[Lit]) -> Vec<u32> {
140 let mut k: Vec<u32> = c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
141 k.sort_unstable();
142 k.dedup();
143 k
144}
145
146pub fn try_assignment_witness(
160 num_vars: usize,
161 db: &[Vec<Lit>],
162 clause: &[Lit],
163 witness: &Witness,
164) -> Option<Vec<Lit>> {
165 let accept = |w: &[Lit]| crate::pr::is_pr(num_vars, db, clause, &Witness::Assignment(w.to_vec()));
166 match witness {
167 Witness::Assignment(w) => {
168 if accept(w) {
169 Some(w.clone())
170 } else {
171 None
172 }
173 }
174 Witness::Substitution(sigma) => {
175 let canon_w: Vec<Lit> = clause.iter().map(|&l| sigma.apply(l).negated()).collect();
177 if accept(&canon_w) {
178 return Some(canon_w);
179 }
180 for &pivot in clause {
183 let mut w = vec![pivot];
184 for &l in &canon_w {
185 if !w.contains(&l) && !w.contains(&l.negated()) {
186 w.push(l);
187 }
188 }
189 if accept(&w) {
190 return Some(w);
191 }
192 }
193 None
194 }
195 }
196}
197
198pub fn emit_drat(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
206 let mut db: Vec<Vec<Lit>> = original.to_vec();
207 let mut out = String::new();
208 for (i, step) in steps.iter().enumerate() {
209 match step {
210 ProofStep::Rup(c) => {
211 if !crate::rup::is_rup(num_vars, &db, c) {
212 return Err(EmitError::StepNotRup { index: i });
213 }
214 push_clause(&mut out, c);
215 out.push('\n');
216 db.push(c.clone());
217 }
218 ProofStep::Delete(c) => {
219 out.push_str("d ");
220 push_clause(&mut out, c);
221 out.push('\n');
222 let key = canon(c);
223 if let Some(pos) = db.iter().position(|d| canon(d) == key) {
224 db.swap_remove(pos);
225 }
226 }
227 ProofStep::Pr { .. } => return Err(EmitError::PrInRupOnlyFormat { index: i }),
228 }
229 }
230 if !crate::rup::is_rup(num_vars, &db, &[]) {
231 return Err(EmitError::EmptyClauseNotRup);
232 }
233 out.push_str("0\n");
234 Ok(out)
235}
236
237pub fn emit_dpr(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
245 let mut db: Vec<Vec<Lit>> = original.to_vec();
246 let mut out = String::new();
247 for (i, step) in steps.iter().enumerate() {
248 match step {
249 ProofStep::Rup(c) => {
250 if !crate::rup::is_rup(num_vars, &db, c) {
251 return Err(EmitError::StepNotRup { index: i });
252 }
253 push_clause(&mut out, c);
254 out.push('\n');
255 db.push(c.clone());
256 }
257 ProofStep::Pr { clause, witness } => {
258 let omega = try_assignment_witness(num_vars, &db, clause, witness)
259 .ok_or(EmitError::RequiresSubstitutionRedundancy { index: i })?;
260 let pivot = clause
263 .iter()
264 .copied()
265 .find(|l| omega.contains(l))
266 .expect("a verified PR witness satisfies its clause");
267 let mut c_ord = vec![pivot];
268 c_ord.extend(clause.iter().copied().filter(|&l| l != pivot));
269 let mut w_ord = vec![pivot];
270 w_ord.extend(omega.iter().copied().filter(|&l| l != pivot));
271 push_clause(&mut out, &c_ord);
272 out.push(' ');
273 for &l in &w_ord {
275 out.push_str(&dimacs(l).to_string());
276 out.push(' ');
277 }
278 out.push_str("0\n");
279 db.push(clause.clone());
280 }
281 ProofStep::Delete(c) => {
282 out.push_str("d ");
283 push_clause(&mut out, c);
284 out.push('\n');
285 let key = canon(c);
286 if let Some(pos) = db.iter().position(|d| canon(d) == key) {
287 db.swap_remove(pos);
288 }
289 }
290 }
291 }
292 if !crate::rup::is_rup(num_vars, &db, &[]) {
293 return Err(EmitError::EmptyClauseNotRup);
294 }
295 out.push_str("0\n");
296 Ok(out)
297}
298
299pub fn emit_sr(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
323 let mut out = String::new();
324 write_sr(&mut out, num_vars, original, steps)?;
325 Ok(out)
326}
327
328pub fn write_sr<W: core::fmt::Write>(
333 w: &mut W,
334 num_vars: usize,
335 original: &[Vec<Lit>],
336 steps: &[ProofStep],
337) -> Result<(), EmitError> {
338 let mut db: Vec<Vec<Lit>> = original.to_vec();
339 for (i, step) in steps.iter().enumerate() {
340 match step {
341 ProofStep::Rup(c) => {
342 if !crate::rup::is_rup(num_vars, &db, c) {
343 return Err(EmitError::StepNotRup { index: i });
344 }
345 wpush_clause(w, c)?;
346 w.write_str("\n")?;
347 db.push(c.clone());
348 }
349 ProofStep::Delete(c) => {
350 w.write_str("d ")?;
351 wpush_clause(w, c)?;
352 w.write_str("\n")?;
353 let key = canon(c);
354 if let Some(pos) = db.iter().position(|d| canon(d) == key) {
355 db.swap_remove(pos);
356 }
357 }
358 ProofStep::Pr { clause, witness } => {
359 if !crate::pr::is_pr(num_vars, &db, clause, witness) {
360 return Err(EmitError::StepNotRup { index: i });
361 }
362 match witness {
363 Witness::Assignment(omega) => {
364 let pivot = *clause.first().ok_or(EmitError::StepNotRup { index: i })?;
365 for &l in clause {
366 wput(w, l)?;
367 }
368 wput(w, pivot)?;
370 for &l in omega {
371 if l != pivot {
372 wput(w, l)?;
373 }
374 }
375 w.write_str("0\n")?;
376 db.push(clause.clone());
377 }
378 Witness::Substitution(sigma) => {
379 let pivot = *clause.first().ok_or(EmitError::StepNotRup { index: i })?;
380 let (omega_extra, fixed_a, fixed_b): (Option<Lit>, u32, u32) =
389 if clause.len() == 1 {
390 let sp = sigma.apply(pivot);
391 (Some(sp.negated()), pivot.var(), sp.var())
392 } else {
393 (None, u32::MAX, u32::MAX)
394 };
395 for &l in clause {
396 wput(w, l)?;
397 }
398 wput(w, pivot)?;
400 if let Some(extra) = omega_extra {
401 if extra != pivot {
402 wput(w, extra)?;
403 }
404 }
405 wput(w, pivot)?;
408 for v in 0..num_vars as u32 {
409 if v == fixed_a || v == fixed_b {
410 continue; }
412 let img = sigma.apply(Lit::pos(v));
413 if img == Lit::pos(v) {
414 continue; }
416 if img == pivot || Lit::pos(v) == pivot {
417 return Err(EmitError::RequiresSubstitutionRedundancy { index: i });
418 }
419 write!(w, "{} ", (v as i64) + 1)?;
420 wput(w, img)?;
421 }
422 w.write_str("0\n")?;
423 db.push(clause.clone());
424 }
425 }
426 }
427 }
428 }
429 if !crate::rup::is_rup(num_vars, &db, &[]) {
430 return Err(EmitError::EmptyClauseNotRup);
431 }
432 w.write_str("0\n")?;
433 Ok(())
434}
435
436pub fn parse_dpr(text: &str) -> Result<Vec<ProofStep>, String> {
440 let mut steps = Vec::new();
441 for raw in text.lines() {
442 let line = raw.trim();
443 if line.is_empty() || line.starts_with('c') {
444 continue;
445 }
446 let mut toks = line.split_whitespace().peekable();
447 let deletion = matches!(toks.peek(), Some(&"d"));
448 if deletion {
449 toks.next();
450 }
451 let mut clause = Vec::new();
453 let mut saw_terminator = false;
454 for tok in toks.by_ref() {
455 let n: i64 = tok.parse().map_err(|_| format!("bad token {tok:?}"))?;
456 if n == 0 {
457 saw_terminator = true;
458 break;
459 }
460 clause.push(Lit::new((n.unsigned_abs() - 1) as u32, n > 0));
461 }
462 if !saw_terminator {
463 return Err(format!("unterminated clause in line {line:?}"));
464 }
465 if deletion {
466 steps.push(ProofStep::Delete(clause));
467 continue;
468 }
469 let mut witness = Vec::new();
471 let mut has_witness = false;
472 for tok in toks.by_ref() {
473 has_witness = true;
474 let n: i64 = tok.parse().map_err(|_| format!("bad witness token {tok:?}"))?;
475 if n == 0 {
476 break;
477 }
478 witness.push(Lit::new((n.unsigned_abs() - 1) as u32, n > 0));
479 }
480 if has_witness {
481 steps.push(ProofStep::Pr { clause, witness: Witness::Assignment(witness) });
482 } else {
483 steps.push(ProofStep::Rup(clause));
484 }
485 }
486 Ok(steps)
487}
488
489fn rup_hints(num_vars: usize, db: &[(u64, Vec<Lit>)], c: &[Lit]) -> Option<Vec<u64>> {
498 let mut assign: Vec<Option<bool>> = vec![None; num_vars];
499 let mut reason: Vec<Option<u64>> = vec![None; num_vars];
500 let mut trail_pos: Vec<Option<usize>> = vec![None; num_vars];
501 let mut next_pos = 0usize;
502 for &l in c {
504 let nl = l.negated();
505 match lit_val(&assign, nl) {
506 Some(true) => {}
507 Some(false) => return Some(Vec::new()),
508 None => {
509 assign[nl.var() as usize] = Some(nl.is_positive());
510 trail_pos[nl.var() as usize] = Some(next_pos);
511 next_pos += 1;
512 }
513 }
514 }
515 loop {
516 let mut changed = false;
517 for (id, clause) in db {
518 let mut satisfied = false;
519 let mut unset: Vec<Lit> = Vec::new();
520 for &l in clause {
521 match lit_val(&assign, l) {
522 Some(true) => {
523 satisfied = true;
524 break;
525 }
526 Some(false) => {}
527 None => {
528 if unset.contains(&l.negated()) {
529 satisfied = true;
530 break;
531 }
532 if !unset.contains(&l) {
533 unset.push(l);
534 }
535 }
536 }
537 }
538 if satisfied {
539 continue;
540 }
541 if unset.is_empty() {
542 return Some(build_chain(db, &reason, &trail_pos, clause, *id));
543 }
544 if unset.len() == 1 {
545 let u = unset[0];
546 assign[u.var() as usize] = Some(u.is_positive());
547 reason[u.var() as usize] = Some(*id);
548 trail_pos[u.var() as usize] = Some(next_pos);
549 next_pos += 1;
550 changed = true;
551 }
552 }
553 if !changed {
554 return None;
555 }
556 }
557}
558
559fn build_chain(
563 db: &[(u64, Vec<Lit>)],
564 reason: &[Option<u64>],
565 trail_pos: &[Option<usize>],
566 conflict_clause: &[Lit],
567 conflict_id: u64,
568) -> Vec<u64> {
569 let by_id: std::collections::HashMap<u64, &Vec<Lit>> =
570 db.iter().map(|(id, c)| (*id, c)).collect();
571 let num_vars = reason.len();
572 let mut visited = vec![false; num_vars];
573 let mut needed: Vec<(usize, u64)> = Vec::new();
574 let mut stack: Vec<u32> = conflict_clause.iter().map(|l| l.var()).collect();
575 while let Some(v) = stack.pop() {
576 if visited[v as usize] {
577 continue;
578 }
579 visited[v as usize] = true;
580 if let Some(rid) = reason[v as usize] {
581 let pos = trail_pos[v as usize].expect("a forced literal has a trail position");
582 needed.push((pos, rid));
583 if let Some(rc) = by_id.get(&rid) {
584 for &l in *rc {
585 stack.push(l.var());
586 }
587 }
588 }
589 }
590 needed.sort_by_key(|&(pos, _)| pos);
591 let mut chain: Vec<u64> = needed.into_iter().map(|(_, id)| id).collect();
592 chain.push(conflict_id);
593 chain
594}
595
596pub fn emit_lrat(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
600 let mut db: Vec<(u64, Vec<Lit>)> =
601 original.iter().enumerate().map(|(i, c)| (i as u64 + 1, c.clone())).collect();
602 let mut next_id = original.len() as u64 + 1;
603 let mut out = String::new();
604 for (i, step) in steps.iter().enumerate() {
605 match step {
606 ProofStep::Rup(c) => {
607 let hints = rup_hints(num_vars, &db, c).ok_or(EmitError::StepNotRup { index: i })?;
608 let id = next_id;
609 next_id += 1;
610 out.push_str(&id.to_string());
611 out.push(' ');
612 push_clause(&mut out, c);
613 out.push(' ');
614 for h in &hints {
615 out.push_str(&h.to_string());
616 out.push(' ');
617 }
618 out.push_str("0\n");
619 db.push((id, c.clone()));
620 }
621 ProofStep::Delete(c) => {
622 let key = canon(c);
623 if let Some(pos) = db.iter().rposition(|(_, d)| canon(d) == key) {
624 let (did, _) = db[pos];
625 out.push_str(&(next_id - 1).to_string());
626 out.push_str(" d ");
627 out.push_str(&did.to_string());
628 out.push_str(" 0\n");
629 db.remove(pos);
630 }
631 }
632 ProofStep::Pr { .. } => return Err(EmitError::PrInRupOnlyFormat { index: i }),
633 }
634 }
635 let ehints = rup_hints(num_vars, &db, &[]).ok_or(EmitError::EmptyClauseNotRup)?;
637 out.push_str(&next_id.to_string());
638 out.push_str(" 0 ");
639 for h in &ehints {
640 out.push_str(&h.to_string());
641 out.push(' ');
642 }
643 out.push_str("0\n");
644 Ok(out)
645}
646
647pub fn check_lrat(num_vars: usize, original: &[Vec<Lit>], lrat: &str) -> bool {
652 let mut db: std::collections::HashMap<u64, Vec<Lit>> =
653 original.iter().enumerate().map(|(i, c)| (i as u64 + 1, c.clone())).collect();
654 for raw in lrat.lines() {
655 let line = raw.trim();
656 if line.is_empty() || line.starts_with('c') {
657 continue;
658 }
659 let mut toks = line.split_whitespace();
660 let Some(id_tok) = toks.next() else { continue };
661 let Ok(id) = id_tok.parse::<u64>() else { return false };
662 let rest: Vec<&str> = toks.collect();
663 if rest.first() == Some(&"d") {
664 for t in &rest[1..] {
665 if let Ok(d) = t.parse::<u64>() {
666 if d != 0 {
667 db.remove(&d);
668 }
669 }
670 }
671 continue;
672 }
673 let mut clause = Vec::new();
675 let mut k = 0usize;
676 while k < rest.len() {
677 let Ok(n) = rest[k].parse::<i64>() else { return false };
678 k += 1;
679 if n == 0 {
680 break;
681 }
682 clause.push(Lit::new((n.unsigned_abs() - 1) as u32, n > 0));
683 }
684 let mut hints = Vec::new();
685 while k < rest.len() {
686 let Ok(n) = rest[k].parse::<i64>() else { return false };
687 k += 1;
688 if n == 0 {
689 break;
690 }
691 hints.push(n as u64);
692 }
693 if !verify_rup_step(num_vars, &db, &clause, &hints) {
694 return false;
695 }
696 if clause.is_empty() {
697 return true;
698 }
699 db.insert(id, clause);
700 }
701 false
702}
703
704fn verify_rup_step(
707 num_vars: usize,
708 db: &std::collections::HashMap<u64, Vec<Lit>>,
709 clause: &[Lit],
710 hints: &[u64],
711) -> bool {
712 let mut assign: Vec<Option<bool>> = vec![None; num_vars];
713 for &l in clause {
714 if !set_true(&mut assign, l.negated()) {
715 return true; }
717 }
718 for (i, &h) in hints.iter().enumerate() {
719 let Some(cl) = db.get(&h) else { return false };
720 let mut satisfied = false;
721 let mut unset: Vec<Lit> = Vec::new();
722 for &l in cl {
723 match lit_val(&assign, l) {
724 Some(true) => {
725 satisfied = true;
726 break;
727 }
728 Some(false) => {}
729 None => {
730 if unset.contains(&l.negated()) {
731 satisfied = true;
732 break;
733 }
734 if !unset.contains(&l) {
735 unset.push(l);
736 }
737 }
738 }
739 }
740 if satisfied {
741 return false; }
743 if unset.is_empty() {
744 return i == hints.len() - 1; }
746 if unset.len() == 1 {
747 set_true(&mut assign, unset[0]);
748 } else {
749 return false; }
751 }
752 false
753}
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use crate::cdcl::Lit;
759 use crate::proof::Perm;
760 use crate::pr::check_pr_refutation;
761
762 fn p(v: u32) -> Lit {
763 Lit::pos(v)
764 }
765 fn n(v: u32) -> Lit {
766 Lit::neg(v)
767 }
768
769 fn pq_unsat() -> (usize, Vec<Vec<Lit>>, Vec<ProofStep>) {
771 let f = vec![vec![p(0), p(1)], vec![p(0), n(1)], vec![n(0), p(1)], vec![n(0), n(1)]];
772 let steps = vec![ProofStep::Rup(vec![p(0)]), ProofStep::Rup(vec![n(0)])];
773 (2, f, steps)
774 }
775
776 #[test]
777 fn drat_round_trips_through_our_checker() {
778 let (nv, f, steps) = pq_unsat();
779 let text = emit_drat(nv, &f, &steps).expect("emits");
780 let parsed = parse_dpr(&text).expect("parses");
781 assert!(check_pr_refutation(nv, &f, &parsed), "round-tripped DRAT must still refute");
782 }
783
784 #[test]
785 fn drat_rejects_a_corrupted_addition() {
786 let f = vec![vec![p(0), p(1)]];
791 let bogus = parse_dpr("-1 0\n").expect("parses"); assert!(!check_pr_refutation(2, &f, &bogus));
793 }
794
795 #[test]
796 fn lrat_hints_validate_and_reject_corruption() {
797 let (nv, f, steps) = pq_unsat();
798 let lrat = emit_lrat(nv, &f, &steps).expect("emits LRAT");
799 assert!(check_lrat(nv, &f, &lrat), "our own LRAT checker accepts our hints");
800
801 let mangled = lrat.replace(" 0\n", " 999 0\n");
803 assert!(!check_lrat(nv, &f, &mangled), "a corrupted hint chain must be rejected");
804 }
805
806 #[test]
807 fn lrat_checker_rejects_a_proof_with_no_empty_clause() {
808 let (nv, f, _) = pq_unsat();
809 let db: Vec<(u64, Vec<Lit>)> =
811 f.iter().enumerate().map(|(i, c)| (i as u64 + 1, c.clone())).collect();
812 let hints = rup_hints(nv, &db, &[p(0)]).expect("p is RUP");
813 let mut line = String::from("5 1 ");
814 for h in hints {
815 line.push_str(&h.to_string());
816 line.push(' ');
817 }
818 line.push_str("0\n");
819 assert!(!check_lrat(nv, &f, &line), "no empty-clause line ⇒ not a refutation");
820 }
821
822 #[test]
823 fn dpr_assignment_witness_line_round_trips_and_reverifies() {
824 let (a, b) = (0u32, 1u32);
827 let f = vec![vec![p(a), p(b)]];
828 let c = vec![n(a), p(b)];
829 let omega = vec![n(a), p(b)];
830 let witness = Witness::Assignment(omega);
831 assert!(crate::pr::is_pr(2, &f, &c, &witness));
832
833 let w = try_assignment_witness(2, &f, &c, &witness).expect("reduces");
836 let pivot = c.iter().copied().find(|l| w.contains(l)).unwrap();
837 let mut out = String::new();
838 let mut c_ord = vec![pivot];
839 c_ord.extend(c.iter().copied().filter(|&l| l != pivot));
840 push_clause(&mut out, &c_ord);
841 out.push(' ');
842 let mut w_ord = vec![pivot];
843 w_ord.extend(w.iter().copied().filter(|&l| l != pivot));
844 for &l in &w_ord {
845 out.push_str(&dimacs(l).to_string());
846 out.push(' ');
847 }
848 out.push_str("0\n");
849
850 let parsed = parse_dpr(&out).expect("parses");
851 match &parsed[0] {
852 ProofStep::Pr { clause, witness } => {
853 assert!(crate::pr::is_pr(2, &f, clause, witness), "parsed DPR line re-verifies");
854 }
855 other => panic!("expected a PR step, got {other:?}"),
856 }
857 }
858
859 #[test]
860 fn lrat_certifies_a_real_cdcl_refutation_of_php() {
861 use crate::cdcl::{SolveResult, Solver};
864 let (cnf, _) = crate::families::php(3);
865 let nv = cnf.num_vars;
866 let mut solver = Solver::new(nv);
867 for c in &cnf.clauses {
868 solver.add_clause(c.clone());
869 }
870 let steps: Vec<ProofStep> = match solver.solve() {
871 SolveResult::Unsat => {
872 solver.learned().iter().map(|c| ProofStep::Rup(c.lits.clone())).collect()
873 }
874 SolveResult::Sat(_) => panic!("PHP(3) is UNSAT"),
875 };
876 let lrat = emit_lrat(nv, &cnf.clauses, &steps).expect("a real CDCL refutation is RUP → LRAT");
877 assert!(check_lrat(nv, &cnf.clauses, &lrat), "LRAT replay of a real CDCL refutation validates");
878
879 let drat = emit_drat(nv, &cnf.clauses, &steps).expect("emits DRAT");
881 let parsed = parse_dpr(&drat).expect("parses");
882 assert!(check_pr_refutation(nv, &cnf.clauses, &parsed));
883 }
884
885 #[test]
886 fn the_universal_lrat_guarantee_via_plain_cdcl() {
887 let (cnf, _) = crate::families::php(4);
890 let nv = cnf.num_vars;
891 let steps = crate::sdcl::plain_cdcl_refutation(nv, &cnf.clauses);
892 let lrat = emit_lrat(nv, &cnf.clauses, &steps).expect("plain CDCL ⇒ RUP ⇒ LRAT");
893 assert!(check_lrat(nv, &cnf.clauses, &lrat), "universal LRAT certificate validates");
894 }
895
896 #[test]
897 fn external_sr2drat_drat_trim_certifies_our_php_symmetry_proof() {
898 let (Ok(sr2drat), Ok(drat_trim)) = (std::env::var("SR2DRAT"), std::env::var("DRAT_TRIM")) else {
904 eprintln!("SR2DRAT/DRAT_TRIM unset — skipping external sr2drat→drat-trim cross-check");
905 return;
906 };
907 for n in [4usize, 8] {
908 let (cnf, _) = crate::families::php(n);
909 let cert = crate::sym_certify::heule_php_refutation(n);
910 assert!(cert.refuted, "PHP({n}) symmetry proof closes internally");
911 let sr = emit_sr(cnf.num_vars, &cnf.clauses, &cert.steps).expect("emits .sr");
912 let cnf_path = temp_write(&format!("logos_sr_php{n}.cnf"), &crate::dimacs::print(&cnf));
913 let sr_path = temp_write(&format!("logos_sr_php{n}.sr"), &sr);
914 let drat_path = cnf_path.with_extension("drat");
915 let drat = std::process::Command::new(&sr2drat)
917 .arg(&cnf_path)
918 .arg(&sr_path)
919 .output()
920 .expect("run sr2drat");
921 std::fs::write(&drat_path, &drat.stdout).expect("write expanded DRAT");
922 let out = std::process::Command::new(&drat_trim)
924 .arg(&cnf_path)
925 .arg(&drat_path)
926 .output()
927 .expect("run drat-trim");
928 let s = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
929 assert!(
930 s.contains("VERIFIED") && !s.contains("NOT VERIFIED"),
931 "sr2drat→drat-trim must VERIFY our PHP({n}) SR proof; got:\n{s}"
932 );
933 }
934 }
935
936 #[test]
937 fn size_sink_measures_the_sr_proof_lazily_and_matches_the_string_length() {
938 let (cnf, _) = crate::families::php(4);
944 let cert = crate::sym_certify::heule_php_refutation(4);
945 assert!(cert.refuted, "PHP(4) symmetry proof closes internally");
946 let full = emit_sr(cnf.num_vars, &cnf.clauses, &cert.steps).expect("emits .sr");
947 assert!(full.len() > 8, "the SR proof is more than a handful of bytes");
948
949 let mut sink = SizeSink::new(64 << 20);
950 write_sr(&mut sink, cnf.num_vars, &cnf.clauses, &cert.steps).expect("streams .sr");
951 assert!(!sink.overflowed());
952 assert_eq!(sink.bytes(), full.len() as u64, "streamed byte count = serialized length");
953
954 let mut capped = SizeSink::new(8);
955 let err = write_sr(&mut capped, cnf.num_vars, &cnf.clauses, &cert.steps);
956 assert_eq!(err, Err(EmitError::SizeCapExceeded), "a tight cap aborts emission early");
957 assert!(capped.overflowed());
958 assert!(capped.bytes() >= 8, "counted up to the cap before bailing");
959 }
960
961 #[test]
962 fn dpr_emits_or_honestly_declines_the_sdcl_proof() {
963 use crate::sdcl::{solve_certified, CertifiedOutcome};
968 let (cnf, _) = crate::families::php(3);
969 let nv = cnf.num_vars;
970 match solve_certified(nv, &cnf.clauses) {
971 CertifiedOutcome::Unsat { steps, .. } => {
972 assert!(check_pr_refutation(nv, &cnf.clauses, &steps), "the SDCL proof self-checks");
973 match emit_dpr(nv, &cnf.clauses, &steps) {
974 Ok(text) => {
975 let parsed = parse_dpr(&text).expect("parses");
976 assert!(check_pr_refutation(nv, &cnf.clauses, &parsed), "round-tripped DPR refutes");
977 }
978 Err(EmitError::RequiresSubstitutionRedundancy { .. }) => {}
979 Err(e) => panic!("unexpected emit error: {e:?}"),
980 }
981 }
982 CertifiedOutcome::Sat(_) => panic!("PHP(3) is UNSAT"),
983 }
984 }
985
986 fn temp_write(name: &str, text: &str) -> std::path::PathBuf {
988 let path = std::env::temp_dir().join(name);
989 std::fs::write(&path, text).expect("write temp file");
990 path
991 }
992
993 #[test]
994 fn external_drat_trim_accepts_our_drat_proof() {
995 let Ok(bin) = std::env::var("DRAT_TRIM") else {
999 eprintln!("DRAT_TRIM unset — skipping external drat-trim cross-check");
1000 return;
1001 };
1002 let (cnf, _) = crate::families::php(5);
1003 let nv = cnf.num_vars;
1004 let steps = crate::sdcl::plain_cdcl_refutation(nv, &cnf.clauses);
1005 let drat = emit_drat(nv, &cnf.clauses, &steps).expect("emits DRAT");
1006 let cnf_path = temp_write("logos_drat_php5.cnf", &crate::dimacs::print(&cnf));
1007 let drat_path = temp_write("logos_drat_php5.drat", &drat);
1008 let out = std::process::Command::new(&bin)
1009 .arg(&cnf_path)
1010 .arg(&drat_path)
1011 .output()
1012 .expect("run drat-trim");
1013 let s = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
1014 assert!(s.contains("VERIFIED") && !s.contains("NOT VERIFIED"), "drat-trim must VERIFY our DRAT; got:\n{s}");
1015 }
1016
1017 #[test]
1018 fn external_lrat_check_accepts_our_lrat_proof() {
1019 let Ok(bin) = std::env::var("LRAT_CHECK") else {
1022 eprintln!("LRAT_CHECK unset — skipping external lrat-check cross-check");
1023 return;
1024 };
1025 let (cnf, _) = crate::families::php(5);
1026 let nv = cnf.num_vars;
1027 let steps = crate::sdcl::plain_cdcl_refutation(nv, &cnf.clauses);
1028 let lrat = emit_lrat(nv, &cnf.clauses, &steps).expect("emits LRAT");
1029 let cnf_path = temp_write("logos_lrat_php5.cnf", &crate::dimacs::print(&cnf));
1030 let lrat_path = temp_write("logos_lrat_php5.lrat", &lrat);
1031 let out = std::process::Command::new(&bin)
1032 .arg(&cnf_path)
1033 .arg(&lrat_path)
1034 .output()
1035 .expect("run lrat-check");
1036 let s = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
1037 assert!(s.contains("VERIFIED") && !s.contains("NOT VERIFIED"), "lrat-check must accept our LRAT; got:\n{s}");
1038 }
1039
1040 #[test]
1041 fn try_assignment_witness_is_sound_on_php_substitutions() {
1042 let cr = crate::sym_certify::heule_php_refutation(4);
1046 let (cnf, _) = crate::families::php(4);
1047 let nv = cnf.num_vars;
1048 let mut db = cnf.clauses.clone();
1049 let mut reduced = 0usize;
1050 let mut sr_only = 0usize;
1051 for step in &cr.steps {
1052 match step {
1053 ProofStep::Pr { clause, witness } => {
1054 if let Some(w) = try_assignment_witness(nv, &db, clause, witness) {
1055 assert!(
1056 crate::pr::is_pr(nv, &db, clause, &Witness::Assignment(w)),
1057 "the reducer returned an unsound assignment witness"
1058 );
1059 reduced += 1;
1060 } else {
1061 sr_only += 1;
1062 }
1063 db.push(clause.clone());
1064 }
1065 ProofStep::Rup(c) => db.push(c.clone()),
1066 ProofStep::Delete(c) => {
1067 let key = canon(c);
1068 if let Some(pos) = db.iter().position(|d| canon(d) == key) {
1069 db.swap_remove(pos);
1070 }
1071 }
1072 }
1073 }
1074 assert!(reduced + sr_only > 0, "the PHP refutation must contain PR steps to classify");
1077 eprintln!("PHP(4): {reduced} steps reduced to PR, {sr_only} remained irreducibly SR");
1078 }
1079}