| //! 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<ProofFormat>, |
| target: BufWriter<Box<dyn Write + 'a>>, |
| checker: Option<Checker<'a>>, |
| 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)?; |
| } |
| } |
| } |