//! Proof generation. use std::io::{self, sink, BufWriter, Write}; use partial_ref::{partial, PartialRef}; use varisat_checker::{internal::SelfChecker, Checker, CheckerError, ProofProcessor}; use varisat_formula::{Lit, Var}; use varisat_internal_proof::{ClauseHash, ProofStep}; use crate::{ context::{parts::*, Context}, solver::SolverError, }; mod drat; mod map_step; /// Proof formats that can be generated during solving. #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum ProofFormat { Varisat, Drat, BinaryDrat, } /// Number of added or removed clauses. pub fn clause_count_delta(step: &ProofStep) -> isize { match step { ProofStep::AddClause { clause } | ProofStep::AtClause { clause, .. } => { if clause.len() > 1 { 1 } else { 0 } } ProofStep::DeleteClause { clause, .. } => { if clause.len() > 1 { -1 } else { 0 } } ProofStep::SolverVarName { .. } | ProofStep::UserVarName { .. } | ProofStep::DeleteVar { .. } | ProofStep::ChangeSamplingMode { .. } | ProofStep::UnitClauses { .. } | ProofStep::ChangeHashBits { .. } | ProofStep::Model { .. } | ProofStep::Assumptions { .. } | ProofStep::FailedAssumptions { .. } | ProofStep::End => 0, } } /// Proof generation. pub struct Proof<'a> { format: Option, target: BufWriter>, checker: Option>, map_step: map_step::MapStep, /// How many bits are used for storing clause hashes. hash_bits: u32, /// How many clauses are currently in the db. /// /// This is used to pick a good number of hash_bits clause_count: isize, } impl<'a> Default for Proof<'a> { fn default() -> Proof<'a> { Proof { format: None, target: BufWriter::new(Box::new(sink())), checker: None, map_step: Default::default(), hash_bits: 64, clause_count: 0, } } } impl<'a> Proof<'a> { /// Start writing proof steps to the given target with the given format. pub fn write_proof(&mut self, target: impl Write + 'a, format: ProofFormat) { self.format = Some(format); self.target = BufWriter::new(Box::new(target)) } /// Begin checking proof steps. pub fn begin_checking(&mut self) { if self.checker.is_none() { self.checker = Some(Checker::new()) } } /// Add a [`ProofProcessor`]. /// /// See also [`Checker::add_processor`]. pub fn add_processor(&mut self, processor: &'a mut dyn ProofProcessor) { self.begin_checking(); self.checker.as_mut().unwrap().add_processor(processor); } /// Whether proof generation is active. pub fn is_active(&self) -> bool { self.checker.is_some() || self.format.is_some() } /// Are we emitting or checking our native format. pub fn native_format(&self) -> bool { self.checker.is_some() || match self.format { Some(ProofFormat::Varisat) => true, _ => false, } } /// Whether clause hashes are required for steps that support them. pub fn clause_hashes_required(&self) -> bool { self.native_format() } /// Whether found models are included in the proof. pub fn models_in_proof(&self) -> bool { self.native_format() } } /// Call when adding an external clause. /// /// This is required for on the fly checking and checking of incremental solving. /// /// *Note:* this function expects the clause to use solver var names. pub fn add_clause<'a>( mut ctx: partial!(Context<'a>, mut ProofP<'a>, mut SolverStateP, VariablesP), clause: &[Lit], ) { if ctx.part(SolverStateP).solver_invoked { add_step(ctx.borrow(), true, &ProofStep::AddClause { clause }) } else { let (variables, mut ctx) = ctx.split_part(VariablesP); let proof = ctx.part_mut(ProofP); if let Some(checker) = &mut proof.checker { let clause = proof.map_step.map_lits(clause, |var| { variables .global_from_solver() .get(var) .expect("no existing global var for solver var") }); let result = checker.add_clause(clause); handle_self_check_result(ctx.borrow(), result); } if clause.len() > 1 { ctx.part_mut(ProofP).clause_count += 1; } } } /// Add a step to the proof. /// /// Ignored when proof generation is disabled. /// /// When `solver_vars` is true, all variables and literals will be converted from solver to global /// vars. Otherwise the proof step needs to use global vars. pub fn add_step<'a, 's>( mut ctx: partial!(Context<'a>, mut ProofP<'a>, mut SolverStateP, VariablesP), solver_vars: bool, step: &'s ProofStep<'s>, ) { if ctx.part(SolverStateP).solver_error.is_some() { return; } if ctx.part(SolverStateP).solver_error.is_some() { return; } let (variables, mut ctx) = ctx.split_part(VariablesP); let proof = ctx.part_mut(ProofP); let map_vars = |var| { if solver_vars { variables .global_from_solver() .get(var) .expect("no existing global var for solver var") } else { var } }; let io_result = match proof.format { Some(ProofFormat::Varisat) => write_varisat_step(ctx.borrow(), map_vars, step), Some(ProofFormat::Drat) => { let step = proof.map_step.map(step, map_vars, |hash| hash); drat::write_step(&mut proof.target, &step) } Some(ProofFormat::BinaryDrat) => { let step = proof.map_step.map(step, map_vars, |hash| hash); drat::write_binary_step(&mut proof.target, &step) } None => Ok(()), }; if io_result.is_ok() { let proof = ctx.part_mut(ProofP); if let Some(checker) = &mut proof.checker { let step = proof.map_step.map(step, map_vars, |hash| hash); let result = checker.self_check_step(step); handle_self_check_result(ctx.borrow(), result); } } handle_io_errors(ctx.borrow(), io_result); } /// Write a step using our native format fn write_varisat_step<'a, 's>( mut ctx: partial!(Context<'a>, mut ProofP<'a>, SolverStateP), map_vars: impl Fn(Var) -> Var, step: &'s ProofStep<'s>, ) -> io::Result<()> { let (proof, ctx) = ctx.split_part_mut(ProofP); proof.clause_count += clause_count_delta(step); let mut rehash = false; // Should we change the hash size? while proof.clause_count > (1 << (proof.hash_bits / 2)) { proof.hash_bits += 2; rehash = true; } if ctx.part(SolverStateP).solver_invoked { while proof.hash_bits > 6 && proof.clause_count * 4 < (1 << (proof.hash_bits / 2)) { proof.hash_bits -= 2; rehash = true; } } if rehash { varisat_internal_proof::binary_format::write_step( &mut proof.target, &ProofStep::ChangeHashBits { bits: proof.hash_bits, }, )?; } let shift_bits = ClauseHash::max_value().count_ones() - proof.hash_bits; let map_hash = |hash| hash >> shift_bits; let step = proof.map_step.map(step, map_vars, map_hash); if proof.format == Some(ProofFormat::Varisat) { varisat_internal_proof::binary_format::write_step(&mut proof.target, &step)?; } Ok(()) } /// Flush buffers used for writing proof steps. pub fn flush_proof<'a>(mut ctx: partial!(Context<'a>, mut ProofP<'a>, mut SolverStateP)) { // We need to explicitly flush to handle IO errors. let result = ctx.part_mut(ProofP).target.flush(); handle_io_errors(ctx.borrow(), result); } /// Stop writing proof steps. pub fn close_proof<'a>( mut ctx: partial!(Context<'a>, mut ProofP<'a>, mut SolverStateP, VariablesP), ) { add_step(ctx.borrow(), true, &ProofStep::End); flush_proof(ctx.borrow()); ctx.part_mut(ProofP).format = None; ctx.part_mut(ProofP).target = BufWriter::new(Box::new(sink())); } /// Called before solve returns to flush buffers and to trigger delayed unit conflict processing. /// /// We flush buffers before solve returns to ensure that we can pass IO errors to the user. pub fn solve_finished<'a>(mut ctx: partial!(Context<'a>, mut ProofP<'a>, mut SolverStateP)) { flush_proof(ctx.borrow()); if let Some(checker) = &mut ctx.part_mut(ProofP).checker { let result = checker.self_check_delayed_steps(); handle_self_check_result(ctx.borrow(), result); } } /// Handle results of on the fly checking. /// /// Panics when the proof is incorrect and aborts solving when a proof processor produced an error. fn handle_self_check_result<'a>( mut ctx: partial!(Context<'a>, mut ProofP<'a>, mut SolverStateP), result: Result<(), CheckerError>, ) { match result { Err(CheckerError::ProofProcessorError { cause }) => { ctx.part_mut(SolverStateP).solver_error = Some(SolverError::ProofProcessorError { cause }); *ctx.part_mut(ProofP) = Proof::default(); } Err(err) => { log::error!("{}", err); if let CheckerError::CheckFailed { debug_step, .. } = err { if !debug_step.is_empty() { log::error!("failed step was {}", debug_step) } } panic!("self check failure"); } Ok(()) => (), } } /// Handle io errors during proof writing. fn handle_io_errors<'a>( mut ctx: partial!(Context<'a>, mut ProofP<'a>, mut SolverStateP), result: io::Result<()>, ) { if let Err(io_err) = result { ctx.part_mut(SolverStateP).solver_error = Some(SolverError::ProofIoError { cause: io_err }); *ctx.part_mut(ProofP) = Proof::default(); } } #[cfg(test)] mod tests { use super::*; use proptest::prelude::*; use std::{fs::File, process::Command}; use tempfile::TempDir; use varisat_dimacs::write_dimacs; use varisat_formula::{test::sgen_unsat_formula, CnfFormula}; use crate::solver::Solver; enum Checker { DratTrim, Rate, } fn test_drat(checker: Checker, formula: CnfFormula, binary: bool) -> Result<(), TestCaseError> { let mut solver = Solver::new(); let tmp = TempDir::new()?; let drat_proof = tmp.path().join("proof.drat"); let cnf_file = tmp.path().join("input.cnf"); write_dimacs(&mut File::create(&cnf_file)?, &formula)?; let format = if binary { ProofFormat::BinaryDrat } else { ProofFormat::Drat }; solver.write_proof(File::create(&drat_proof)?, format); solver.add_formula(&formula); prop_assert_eq!(solver.solve().ok(), Some(false)); solver .close_proof() .map_err(|e| TestCaseError::fail(e.to_string()))?; let output = match checker { Checker::DratTrim => { if binary { Command::new("drat-trim") .arg(&cnf_file) .arg(&drat_proof) .arg("-i") .output()? } else { Command::new("drat-trim") .arg(&cnf_file) .arg(&drat_proof) .output()? } } Checker::Rate => Command::new("rate") .arg(&cnf_file) .arg(&drat_proof) .output()?, }; prop_assert!(std::str::from_utf8(&output.stdout)?.contains("s VERIFIED")); Ok(()) } proptest! { #[cfg_attr(not(test_drat_trim), ignore)] #[test] fn sgen_unsat_drat_trim( formula in sgen_unsat_formula(1..7usize), ) { test_drat(Checker::DratTrim, formula, false)?; } #[cfg_attr(not(test_drat_trim), ignore)] #[test] fn sgen_unsat_binary_drat_trim( formula in sgen_unsat_formula(1..7usize), ) { test_drat(Checker::DratTrim, formula, true)?; } #[cfg_attr(not(test_rate), ignore)] #[test] fn sgen_unsat_rate( formula in sgen_unsat_formula(1..7usize), ) { test_drat(Checker::Rate, formula, false)?; } #[cfg_attr(not(test_rate), ignore)] #[test] fn sgen_unsat_binary_rate( formula in sgen_unsat_formula(1..7usize), ) { test_drat(Checker::Rate, formula, true)?; } } }