blob: 336f7fbc2b28eaac88245a292f79ba7ddf50297e [file] [log] [blame] [edit]
//! 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)?;
}
}
}