use core::fmt::Write as _;
use crate::cdcl::Lit;
use crate::proof::{ProofStep, Witness};
use crate::rup::{lit_val, set_true};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EmitError {
StepNotRup { index: usize },
EmptyClauseNotRup,
RequiresSubstitutionRedundancy { index: usize },
PrInRupOnlyFormat { index: usize },
SizeCapExceeded,
}
impl From<core::fmt::Error> for EmitError {
fn from(_: core::fmt::Error) -> Self {
EmitError::SizeCapExceeded
}
}
pub struct SizeSink {
bytes: u64,
cap: u64,
overflowed: bool,
}
impl SizeSink {
pub fn new(cap: u64) -> Self {
Self { bytes: 0, cap, overflowed: false }
}
pub fn bytes(&self) -> u64 {
self.bytes
}
pub fn overflowed(&self) -> bool {
self.overflowed
}
}
impl core::fmt::Write for SizeSink {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
self.bytes = self.bytes.saturating_add(s.len() as u64);
if self.bytes > self.cap {
self.overflowed = true;
return Err(core::fmt::Error);
}
Ok(())
}
}
#[inline]
fn dimacs(l: Lit) -> i64 {
let v = (l.var() + 1) as i64;
if l.is_positive() {
v
} else {
-v
}
}
fn push_clause(out: &mut String, lits: &[Lit]) {
for &l in lits {
out.push_str(&dimacs(l).to_string());
out.push(' ');
}
out.push_str("0");
}
fn wpush_clause<W: core::fmt::Write>(w: &mut W, lits: &[Lit]) -> Result<(), EmitError> {
for &l in lits {
write!(w, "{} ", dimacs(l))?;
}
w.write_str("0")?;
Ok(())
}
fn wput<W: core::fmt::Write>(w: &mut W, l: Lit) -> Result<(), EmitError> {
write!(w, "{} ", dimacs(l))?;
Ok(())
}
fn canon(c: &[Lit]) -> Vec<u32> {
let mut k: Vec<u32> = c.iter().map(|l| l.var() * 2 + u32::from(!l.is_positive())).collect();
k.sort_unstable();
k.dedup();
k
}
pub fn try_assignment_witness(
num_vars: usize,
db: &[Vec<Lit>],
clause: &[Lit],
witness: &Witness,
) -> Option<Vec<Lit>> {
let accept = |w: &[Lit]| crate::pr::is_pr(num_vars, db, clause, &Witness::Assignment(w.to_vec()));
match witness {
Witness::Assignment(w) => {
if accept(w) {
Some(w.clone())
} else {
None
}
}
Witness::Substitution(sigma) => {
let canon_w: Vec<Lit> = clause.iter().map(|&l| sigma.apply(l).negated()).collect();
if accept(&canon_w) {
return Some(canon_w);
}
for &pivot in clause {
let mut w = vec![pivot];
for &l in &canon_w {
if !w.contains(&l) && !w.contains(&l.negated()) {
w.push(l);
}
}
if accept(&w) {
return Some(w);
}
}
None
}
}
}
pub fn emit_drat(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
let mut db: Vec<Vec<Lit>> = original.to_vec();
let mut out = String::new();
for (i, step) in steps.iter().enumerate() {
match step {
ProofStep::Rup(c) => {
if !crate::rup::is_rup(num_vars, &db, c) {
return Err(EmitError::StepNotRup { index: i });
}
push_clause(&mut out, c);
out.push('\n');
db.push(c.clone());
}
ProofStep::Delete(c) => {
out.push_str("d ");
push_clause(&mut out, c);
out.push('\n');
let key = canon(c);
if let Some(pos) = db.iter().position(|d| canon(d) == key) {
db.swap_remove(pos);
}
}
ProofStep::Pr { .. } => return Err(EmitError::PrInRupOnlyFormat { index: i }),
}
}
if !crate::rup::is_rup(num_vars, &db, &[]) {
return Err(EmitError::EmptyClauseNotRup);
}
out.push_str("0\n");
Ok(out)
}
pub fn emit_dpr(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
let mut db: Vec<Vec<Lit>> = original.to_vec();
let mut out = String::new();
for (i, step) in steps.iter().enumerate() {
match step {
ProofStep::Rup(c) => {
if !crate::rup::is_rup(num_vars, &db, c) {
return Err(EmitError::StepNotRup { index: i });
}
push_clause(&mut out, c);
out.push('\n');
db.push(c.clone());
}
ProofStep::Pr { clause, witness } => {
let omega = try_assignment_witness(num_vars, &db, clause, witness)
.ok_or(EmitError::RequiresSubstitutionRedundancy { index: i })?;
let pivot = clause
.iter()
.copied()
.find(|l| omega.contains(l))
.expect("a verified PR witness satisfies its clause");
let mut c_ord = vec![pivot];
c_ord.extend(clause.iter().copied().filter(|&l| l != pivot));
let mut w_ord = vec![pivot];
w_ord.extend(omega.iter().copied().filter(|&l| l != pivot));
push_clause(&mut out, &c_ord);
out.push(' ');
for &l in &w_ord {
out.push_str(&dimacs(l).to_string());
out.push(' ');
}
out.push_str("0\n");
db.push(clause.clone());
}
ProofStep::Delete(c) => {
out.push_str("d ");
push_clause(&mut out, c);
out.push('\n');
let key = canon(c);
if let Some(pos) = db.iter().position(|d| canon(d) == key) {
db.swap_remove(pos);
}
}
}
}
if !crate::rup::is_rup(num_vars, &db, &[]) {
return Err(EmitError::EmptyClauseNotRup);
}
out.push_str("0\n");
Ok(out)
}
pub fn emit_sr(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
let mut out = String::new();
write_sr(&mut out, num_vars, original, steps)?;
Ok(out)
}
pub fn write_sr<W: core::fmt::Write>(
w: &mut W,
num_vars: usize,
original: &[Vec<Lit>],
steps: &[ProofStep],
) -> Result<(), EmitError> {
let mut db: Vec<Vec<Lit>> = original.to_vec();
for (i, step) in steps.iter().enumerate() {
match step {
ProofStep::Rup(c) => {
if !crate::rup::is_rup(num_vars, &db, c) {
return Err(EmitError::StepNotRup { index: i });
}
wpush_clause(w, c)?;
w.write_str("\n")?;
db.push(c.clone());
}
ProofStep::Delete(c) => {
w.write_str("d ")?;
wpush_clause(w, c)?;
w.write_str("\n")?;
let key = canon(c);
if let Some(pos) = db.iter().position(|d| canon(d) == key) {
db.swap_remove(pos);
}
}
ProofStep::Pr { clause, witness } => {
if !crate::pr::is_pr(num_vars, &db, clause, witness) {
return Err(EmitError::StepNotRup { index: i });
}
match witness {
Witness::Assignment(omega) => {
let pivot = *clause.first().ok_or(EmitError::StepNotRup { index: i })?;
for &l in clause {
wput(w, l)?;
}
wput(w, pivot)?;
for &l in omega {
if l != pivot {
wput(w, l)?;
}
}
w.write_str("0\n")?;
db.push(clause.clone());
}
Witness::Substitution(sigma) => {
let pivot = *clause.first().ok_or(EmitError::StepNotRup { index: i })?;
let (omega_extra, fixed_a, fixed_b): (Option<Lit>, u32, u32) =
if clause.len() == 1 {
let sp = sigma.apply(pivot);
(Some(sp.negated()), pivot.var(), sp.var())
} else {
(None, u32::MAX, u32::MAX)
};
for &l in clause {
wput(w, l)?;
}
wput(w, pivot)?;
if let Some(extra) = omega_extra {
if extra != pivot {
wput(w, extra)?;
}
}
wput(w, pivot)?;
for v in 0..num_vars as u32 {
if v == fixed_a || v == fixed_b {
continue; }
let img = sigma.apply(Lit::pos(v));
if img == Lit::pos(v) {
continue; }
if img == pivot || Lit::pos(v) == pivot {
return Err(EmitError::RequiresSubstitutionRedundancy { index: i });
}
write!(w, "{} ", (v as i64) + 1)?;
wput(w, img)?;
}
w.write_str("0\n")?;
db.push(clause.clone());
}
}
}
}
}
if !crate::rup::is_rup(num_vars, &db, &[]) {
return Err(EmitError::EmptyClauseNotRup);
}
w.write_str("0\n")?;
Ok(())
}
pub fn parse_dpr(text: &str) -> Result<Vec<ProofStep>, String> {
let mut steps = Vec::new();
for raw in text.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('c') {
continue;
}
let mut toks = line.split_whitespace().peekable();
let deletion = matches!(toks.peek(), Some(&"d"));
if deletion {
toks.next();
}
let mut clause = Vec::new();
let mut saw_terminator = false;
for tok in toks.by_ref() {
let n: i64 = tok.parse().map_err(|_| format!("bad token {tok:?}"))?;
if n == 0 {
saw_terminator = true;
break;
}
clause.push(Lit::new((n.unsigned_abs() - 1) as u32, n > 0));
}
if !saw_terminator {
return Err(format!("unterminated clause in line {line:?}"));
}
if deletion {
steps.push(ProofStep::Delete(clause));
continue;
}
let mut witness = Vec::new();
let mut has_witness = false;
for tok in toks.by_ref() {
has_witness = true;
let n: i64 = tok.parse().map_err(|_| format!("bad witness token {tok:?}"))?;
if n == 0 {
break;
}
witness.push(Lit::new((n.unsigned_abs() - 1) as u32, n > 0));
}
if has_witness {
steps.push(ProofStep::Pr { clause, witness: Witness::Assignment(witness) });
} else {
steps.push(ProofStep::Rup(clause));
}
}
Ok(steps)
}
fn rup_hints(num_vars: usize, db: &[(u64, Vec<Lit>)], c: &[Lit]) -> Option<Vec<u64>> {
let mut assign: Vec<Option<bool>> = vec![None; num_vars];
let mut reason: Vec<Option<u64>> = vec![None; num_vars];
let mut trail_pos: Vec<Option<usize>> = vec![None; num_vars];
let mut next_pos = 0usize;
for &l in c {
let nl = l.negated();
match lit_val(&assign, nl) {
Some(true) => {}
Some(false) => return Some(Vec::new()),
None => {
assign[nl.var() as usize] = Some(nl.is_positive());
trail_pos[nl.var() as usize] = Some(next_pos);
next_pos += 1;
}
}
}
loop {
let mut changed = false;
for (id, clause) in db {
let mut satisfied = false;
let mut unset: Vec<Lit> = Vec::new();
for &l in clause {
match lit_val(&assign, l) {
Some(true) => {
satisfied = true;
break;
}
Some(false) => {}
None => {
if unset.contains(&l.negated()) {
satisfied = true;
break;
}
if !unset.contains(&l) {
unset.push(l);
}
}
}
}
if satisfied {
continue;
}
if unset.is_empty() {
return Some(build_chain(db, &reason, &trail_pos, clause, *id));
}
if unset.len() == 1 {
let u = unset[0];
assign[u.var() as usize] = Some(u.is_positive());
reason[u.var() as usize] = Some(*id);
trail_pos[u.var() as usize] = Some(next_pos);
next_pos += 1;
changed = true;
}
}
if !changed {
return None;
}
}
}
fn build_chain(
db: &[(u64, Vec<Lit>)],
reason: &[Option<u64>],
trail_pos: &[Option<usize>],
conflict_clause: &[Lit],
conflict_id: u64,
) -> Vec<u64> {
let by_id: std::collections::HashMap<u64, &Vec<Lit>> =
db.iter().map(|(id, c)| (*id, c)).collect();
let num_vars = reason.len();
let mut visited = vec![false; num_vars];
let mut needed: Vec<(usize, u64)> = Vec::new();
let mut stack: Vec<u32> = conflict_clause.iter().map(|l| l.var()).collect();
while let Some(v) = stack.pop() {
if visited[v as usize] {
continue;
}
visited[v as usize] = true;
if let Some(rid) = reason[v as usize] {
let pos = trail_pos[v as usize].expect("a forced literal has a trail position");
needed.push((pos, rid));
if let Some(rc) = by_id.get(&rid) {
for &l in *rc {
stack.push(l.var());
}
}
}
}
needed.sort_by_key(|&(pos, _)| pos);
let mut chain: Vec<u64> = needed.into_iter().map(|(_, id)| id).collect();
chain.push(conflict_id);
chain
}
pub fn emit_lrat(num_vars: usize, original: &[Vec<Lit>], steps: &[ProofStep]) -> Result<String, EmitError> {
let mut db: Vec<(u64, Vec<Lit>)> =
original.iter().enumerate().map(|(i, c)| (i as u64 + 1, c.clone())).collect();
let mut next_id = original.len() as u64 + 1;
let mut out = String::new();
for (i, step) in steps.iter().enumerate() {
match step {
ProofStep::Rup(c) => {
let hints = rup_hints(num_vars, &db, c).ok_or(EmitError::StepNotRup { index: i })?;
let id = next_id;
next_id += 1;
out.push_str(&id.to_string());
out.push(' ');
push_clause(&mut out, c);
out.push(' ');
for h in &hints {
out.push_str(&h.to_string());
out.push(' ');
}
out.push_str("0\n");
db.push((id, c.clone()));
}
ProofStep::Delete(c) => {
let key = canon(c);
if let Some(pos) = db.iter().rposition(|(_, d)| canon(d) == key) {
let (did, _) = db[pos];
out.push_str(&(next_id - 1).to_string());
out.push_str(" d ");
out.push_str(&did.to_string());
out.push_str(" 0\n");
db.remove(pos);
}
}
ProofStep::Pr { .. } => return Err(EmitError::PrInRupOnlyFormat { index: i }),
}
}
let ehints = rup_hints(num_vars, &db, &[]).ok_or(EmitError::EmptyClauseNotRup)?;
out.push_str(&next_id.to_string());
out.push_str(" 0 ");
for h in &ehints {
out.push_str(&h.to_string());
out.push(' ');
}
out.push_str("0\n");
Ok(out)
}
pub fn check_lrat(num_vars: usize, original: &[Vec<Lit>], lrat: &str) -> bool {
let mut db: std::collections::HashMap<u64, Vec<Lit>> =
original.iter().enumerate().map(|(i, c)| (i as u64 + 1, c.clone())).collect();
for raw in lrat.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('c') {
continue;
}
let mut toks = line.split_whitespace();
let Some(id_tok) = toks.next() else { continue };
let Ok(id) = id_tok.parse::<u64>() else { return false };
let rest: Vec<&str> = toks.collect();
if rest.first() == Some(&"d") {
for t in &rest[1..] {
if let Ok(d) = t.parse::<u64>() {
if d != 0 {
db.remove(&d);
}
}
}
continue;
}
let mut clause = Vec::new();
let mut k = 0usize;
while k < rest.len() {
let Ok(n) = rest[k].parse::<i64>() else { return false };
k += 1;
if n == 0 {
break;
}
clause.push(Lit::new((n.unsigned_abs() - 1) as u32, n > 0));
}
let mut hints = Vec::new();
while k < rest.len() {
let Ok(n) = rest[k].parse::<i64>() else { return false };
k += 1;
if n == 0 {
break;
}
hints.push(n as u64);
}
if !verify_rup_step(num_vars, &db, &clause, &hints) {
return false;
}
if clause.is_empty() {
return true;
}
db.insert(id, clause);
}
false
}
fn verify_rup_step(
num_vars: usize,
db: &std::collections::HashMap<u64, Vec<Lit>>,
clause: &[Lit],
hints: &[u64],
) -> bool {
let mut assign: Vec<Option<bool>> = vec![None; num_vars];
for &l in clause {
if !set_true(&mut assign, l.negated()) {
return true; }
}
for (i, &h) in hints.iter().enumerate() {
let Some(cl) = db.get(&h) else { return false };
let mut satisfied = false;
let mut unset: Vec<Lit> = Vec::new();
for &l in cl {
match lit_val(&assign, l) {
Some(true) => {
satisfied = true;
break;
}
Some(false) => {}
None => {
if unset.contains(&l.negated()) {
satisfied = true;
break;
}
if !unset.contains(&l) {
unset.push(l);
}
}
}
}
if satisfied {
return false; }
if unset.is_empty() {
return i == hints.len() - 1; }
if unset.len() == 1 {
set_true(&mut assign, unset[0]);
} else {
return false; }
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cdcl::Lit;
use crate::proof::Perm;
use crate::pr::check_pr_refutation;
fn p(v: u32) -> Lit {
Lit::pos(v)
}
fn n(v: u32) -> Lit {
Lit::neg(v)
}
fn pq_unsat() -> (usize, Vec<Vec<Lit>>, Vec<ProofStep>) {
let f = vec![vec![p(0), p(1)], vec![p(0), n(1)], vec![n(0), p(1)], vec![n(0), n(1)]];
let steps = vec![ProofStep::Rup(vec![p(0)]), ProofStep::Rup(vec![n(0)])];
(2, f, steps)
}
#[test]
fn drat_round_trips_through_our_checker() {
let (nv, f, steps) = pq_unsat();
let text = emit_drat(nv, &f, &steps).expect("emits");
let parsed = parse_dpr(&text).expect("parses");
assert!(check_pr_refutation(nv, &f, &parsed), "round-tripped DRAT must still refute");
}
#[test]
fn drat_rejects_a_corrupted_addition() {
let f = vec![vec![p(0), p(1)]];
let bogus = parse_dpr("-1 0\n").expect("parses"); assert!(!check_pr_refutation(2, &f, &bogus));
}
#[test]
fn lrat_hints_validate_and_reject_corruption() {
let (nv, f, steps) = pq_unsat();
let lrat = emit_lrat(nv, &f, &steps).expect("emits LRAT");
assert!(check_lrat(nv, &f, &lrat), "our own LRAT checker accepts our hints");
let mangled = lrat.replace(" 0\n", " 999 0\n");
assert!(!check_lrat(nv, &f, &mangled), "a corrupted hint chain must be rejected");
}
#[test]
fn lrat_checker_rejects_a_proof_with_no_empty_clause() {
let (nv, f, _) = pq_unsat();
let db: Vec<(u64, Vec<Lit>)> =
f.iter().enumerate().map(|(i, c)| (i as u64 + 1, c.clone())).collect();
let hints = rup_hints(nv, &db, &[p(0)]).expect("p is RUP");
let mut line = String::from("5 1 ");
for h in hints {
line.push_str(&h.to_string());
line.push(' ');
}
line.push_str("0\n");
assert!(!check_lrat(nv, &f, &line), "no empty-clause line ⇒ not a refutation");
}
#[test]
fn dpr_assignment_witness_line_round_trips_and_reverifies() {
let (a, b) = (0u32, 1u32);
let f = vec![vec![p(a), p(b)]];
let c = vec![n(a), p(b)];
let omega = vec![n(a), p(b)];
let witness = Witness::Assignment(omega);
assert!(crate::pr::is_pr(2, &f, &c, &witness));
let w = try_assignment_witness(2, &f, &c, &witness).expect("reduces");
let pivot = c.iter().copied().find(|l| w.contains(l)).unwrap();
let mut out = String::new();
let mut c_ord = vec![pivot];
c_ord.extend(c.iter().copied().filter(|&l| l != pivot));
push_clause(&mut out, &c_ord);
out.push(' ');
let mut w_ord = vec![pivot];
w_ord.extend(w.iter().copied().filter(|&l| l != pivot));
for &l in &w_ord {
out.push_str(&dimacs(l).to_string());
out.push(' ');
}
out.push_str("0\n");
let parsed = parse_dpr(&out).expect("parses");
match &parsed[0] {
ProofStep::Pr { clause, witness } => {
assert!(crate::pr::is_pr(2, &f, clause, witness), "parsed DPR line re-verifies");
}
other => panic!("expected a PR step, got {other:?}"),
}
}
#[test]
fn lrat_certifies_a_real_cdcl_refutation_of_php() {
use crate::cdcl::{SolveResult, Solver};
let (cnf, _) = crate::families::php(3);
let nv = cnf.num_vars;
let mut solver = Solver::new(nv);
for c in &cnf.clauses {
solver.add_clause(c.clone());
}
let steps: Vec<ProofStep> = match solver.solve() {
SolveResult::Unsat => {
solver.learned().iter().map(|c| ProofStep::Rup(c.lits.clone())).collect()
}
SolveResult::Sat(_) => panic!("PHP(3) is UNSAT"),
};
let lrat = emit_lrat(nv, &cnf.clauses, &steps).expect("a real CDCL refutation is RUP → LRAT");
assert!(check_lrat(nv, &cnf.clauses, &lrat), "LRAT replay of a real CDCL refutation validates");
let drat = emit_drat(nv, &cnf.clauses, &steps).expect("emits DRAT");
let parsed = parse_dpr(&drat).expect("parses");
assert!(check_pr_refutation(nv, &cnf.clauses, &parsed));
}
#[test]
fn the_universal_lrat_guarantee_via_plain_cdcl() {
let (cnf, _) = crate::families::php(4);
let nv = cnf.num_vars;
let steps = crate::sdcl::plain_cdcl_refutation(nv, &cnf.clauses);
let lrat = emit_lrat(nv, &cnf.clauses, &steps).expect("plain CDCL ⇒ RUP ⇒ LRAT");
assert!(check_lrat(nv, &cnf.clauses, &lrat), "universal LRAT certificate validates");
}
#[test]
fn external_sr2drat_drat_trim_certifies_our_php_symmetry_proof() {
let (Ok(sr2drat), Ok(drat_trim)) = (std::env::var("SR2DRAT"), std::env::var("DRAT_TRIM")) else {
eprintln!("SR2DRAT/DRAT_TRIM unset — skipping external sr2drat→drat-trim cross-check");
return;
};
for n in [4usize, 8] {
let (cnf, _) = crate::families::php(n);
let cert = crate::sym_certify::heule_php_refutation(n);
assert!(cert.refuted, "PHP({n}) symmetry proof closes internally");
let sr = emit_sr(cnf.num_vars, &cnf.clauses, &cert.steps).expect("emits .sr");
let cnf_path = temp_write(&format!("logos_sr_php{n}.cnf"), &crate::dimacs::print(&cnf));
let sr_path = temp_write(&format!("logos_sr_php{n}.sr"), &sr);
let drat_path = cnf_path.with_extension("drat");
let drat = std::process::Command::new(&sr2drat)
.arg(&cnf_path)
.arg(&sr_path)
.output()
.expect("run sr2drat");
std::fs::write(&drat_path, &drat.stdout).expect("write expanded DRAT");
let out = std::process::Command::new(&drat_trim)
.arg(&cnf_path)
.arg(&drat_path)
.output()
.expect("run drat-trim");
let s = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
assert!(
s.contains("VERIFIED") && !s.contains("NOT VERIFIED"),
"sr2drat→drat-trim must VERIFY our PHP({n}) SR proof; got:\n{s}"
);
}
}
#[test]
fn size_sink_measures_the_sr_proof_lazily_and_matches_the_string_length() {
let (cnf, _) = crate::families::php(4);
let cert = crate::sym_certify::heule_php_refutation(4);
assert!(cert.refuted, "PHP(4) symmetry proof closes internally");
let full = emit_sr(cnf.num_vars, &cnf.clauses, &cert.steps).expect("emits .sr");
assert!(full.len() > 8, "the SR proof is more than a handful of bytes");
let mut sink = SizeSink::new(64 << 20);
write_sr(&mut sink, cnf.num_vars, &cnf.clauses, &cert.steps).expect("streams .sr");
assert!(!sink.overflowed());
assert_eq!(sink.bytes(), full.len() as u64, "streamed byte count = serialized length");
let mut capped = SizeSink::new(8);
let err = write_sr(&mut capped, cnf.num_vars, &cnf.clauses, &cert.steps);
assert_eq!(err, Err(EmitError::SizeCapExceeded), "a tight cap aborts emission early");
assert!(capped.overflowed());
assert!(capped.bytes() >= 8, "counted up to the cap before bailing");
}
#[test]
fn dpr_emits_or_honestly_declines_the_sdcl_proof() {
use crate::sdcl::{solve_certified, CertifiedOutcome};
let (cnf, _) = crate::families::php(3);
let nv = cnf.num_vars;
match solve_certified(nv, &cnf.clauses) {
CertifiedOutcome::Unsat { steps, .. } => {
assert!(check_pr_refutation(nv, &cnf.clauses, &steps), "the SDCL proof self-checks");
match emit_dpr(nv, &cnf.clauses, &steps) {
Ok(text) => {
let parsed = parse_dpr(&text).expect("parses");
assert!(check_pr_refutation(nv, &cnf.clauses, &parsed), "round-tripped DPR refutes");
}
Err(EmitError::RequiresSubstitutionRedundancy { .. }) => {}
Err(e) => panic!("unexpected emit error: {e:?}"),
}
}
CertifiedOutcome::Sat(_) => panic!("PHP(3) is UNSAT"),
}
}
fn temp_write(name: &str, text: &str) -> std::path::PathBuf {
let path = std::env::temp_dir().join(name);
std::fs::write(&path, text).expect("write temp file");
path
}
#[test]
fn external_drat_trim_accepts_our_drat_proof() {
let Ok(bin) = std::env::var("DRAT_TRIM") else {
eprintln!("DRAT_TRIM unset — skipping external drat-trim cross-check");
return;
};
let (cnf, _) = crate::families::php(5);
let nv = cnf.num_vars;
let steps = crate::sdcl::plain_cdcl_refutation(nv, &cnf.clauses);
let drat = emit_drat(nv, &cnf.clauses, &steps).expect("emits DRAT");
let cnf_path = temp_write("logos_drat_php5.cnf", &crate::dimacs::print(&cnf));
let drat_path = temp_write("logos_drat_php5.drat", &drat);
let out = std::process::Command::new(&bin)
.arg(&cnf_path)
.arg(&drat_path)
.output()
.expect("run drat-trim");
let s = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
assert!(s.contains("VERIFIED") && !s.contains("NOT VERIFIED"), "drat-trim must VERIFY our DRAT; got:\n{s}");
}
#[test]
fn external_lrat_check_accepts_our_lrat_proof() {
let Ok(bin) = std::env::var("LRAT_CHECK") else {
eprintln!("LRAT_CHECK unset — skipping external lrat-check cross-check");
return;
};
let (cnf, _) = crate::families::php(5);
let nv = cnf.num_vars;
let steps = crate::sdcl::plain_cdcl_refutation(nv, &cnf.clauses);
let lrat = emit_lrat(nv, &cnf.clauses, &steps).expect("emits LRAT");
let cnf_path = temp_write("logos_lrat_php5.cnf", &crate::dimacs::print(&cnf));
let lrat_path = temp_write("logos_lrat_php5.lrat", &lrat);
let out = std::process::Command::new(&bin)
.arg(&cnf_path)
.arg(&lrat_path)
.output()
.expect("run lrat-check");
let s = format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
assert!(s.contains("VERIFIED") && !s.contains("NOT VERIFIED"), "lrat-check must accept our LRAT; got:\n{s}");
}
#[test]
fn try_assignment_witness_is_sound_on_php_substitutions() {
let cr = crate::sym_certify::heule_php_refutation(4);
let (cnf, _) = crate::families::php(4);
let nv = cnf.num_vars;
let mut db = cnf.clauses.clone();
let mut reduced = 0usize;
let mut sr_only = 0usize;
for step in &cr.steps {
match step {
ProofStep::Pr { clause, witness } => {
if let Some(w) = try_assignment_witness(nv, &db, clause, witness) {
assert!(
crate::pr::is_pr(nv, &db, clause, &Witness::Assignment(w)),
"the reducer returned an unsound assignment witness"
);
reduced += 1;
} else {
sr_only += 1;
}
db.push(clause.clone());
}
ProofStep::Rup(c) => db.push(c.clone()),
ProofStep::Delete(c) => {
let key = canon(c);
if let Some(pos) = db.iter().position(|d| canon(d) == key) {
db.swap_remove(pos);
}
}
}
}
assert!(reduced + sr_only > 0, "the PHP refutation must contain PR steps to classify");
eprintln!("PHP(4): {reduced} steps reduced to PR, {sr_only} remained irreducibly SR");
}
}