blob: 7f3e7478f72335ea8c57df2bf2970e1382eb0681 [file] [log] [blame] [edit]
//! Checker state and checking of proof steps.
use std::{io, mem::replace};
use partial_ref::{partial, PartialRef};
use rustc_hash::FxHashSet as HashSet;
use varisat_formula::{Lit, Var};
use varisat_internal_proof::{binary_format::Parser, ClauseHash, DeleteClauseProof, ProofStep};
use crate::{
clauses::{
add_clause, delete_clause, store_clause, store_unit_clause, DeleteClauseResult,
StoreClauseResult, UnitClause, UnitId,
},
context::{parts::*, Context},
hash::rehash,
processing::{
process_step, CheckedProofStep, CheckedSamplingMode, CheckedUserVar, ResolutionPropagations,
},
rup::check_clause_with_hashes,
sorted_lits::{copy_canonical, is_subset},
variables::{
add_user_mapping, ensure_sampling_var, ensure_var, remove_user_mapping, SamplingMode,
VarData,
},
CheckerError,
};
/// A checker for unsatisfiability proofs in the native varisat format.
#[derive(Default)]
pub struct CheckerState {
/// Current step number.
pub step: u64,
/// Whether unsatisfiability was proven.
pub unsat: bool,
/// Whether an end of proof step was checked.
ended: bool,
/// Last added irredundant clause id.
///
/// Sorted and free of duplicates.
previous_irred_clause_id: Option<u64>,
/// Last added irredundant clause literals.
previous_irred_clause_lits: Vec<Lit>,
/// Current assumptions, used to check FailedAssumptions and Model
assumptions: Vec<Lit>,
}
impl CheckerState {
/// Check whether a given clause is subsumed by the last added irredundant clause.
///
/// `lits` must be sorted and free of duplicates.
fn subsumed_by_previous_irred_clause(&self, lits: &[Lit]) -> bool {
if self.previous_irred_clause_id.is_none() {
return false;
}
is_subset(&self.previous_irred_clause_lits[..], lits, true)
}
}
/// Check a single proof step
pub fn check_step<'a>(
mut ctx: partial!(
Context<'a>,
mut CheckerStateP,
mut ClauseHasherP,
mut ClausesP,
mut ProcessingP<'a>,
mut RupCheckP,
mut TmpDataP,
mut VariablesP,
),
step: ProofStep,
) -> Result<(), CheckerError> {
let mut result = match step {
ProofStep::SolverVarName { global, solver } => {
ctx.part_mut(ClauseHasherP)
.buffered_solver_var_names
.push((global, solver));
if solver.is_some() {
ctx.part_mut(ClauseHasherP)
.rename_in_buffered_solver_var_names = true;
}
Ok(())
}
ProofStep::UserVarName { global, user } => {
if let Some(user) = user {
add_user_mapping(ctx.borrow(), global, user)?;
} else {
remove_user_mapping(ctx.borrow(), global)?;
}
Ok(())
}
ProofStep::DeleteVar { var } => check_delete_var_step(ctx.borrow(), var),
ProofStep::ChangeSamplingMode { var, sample } => {
check_change_sampling_mode(ctx.borrow(), var, sample)
}
ProofStep::AddClause { clause } => add_clause(ctx.borrow(), clause),
ProofStep::AtClause {
redundant,
clause,
propagation_hashes,
} => check_at_clause_step(ctx.borrow(), redundant, clause, propagation_hashes),
ProofStep::DeleteClause { clause, proof } => {
check_delete_clause_step(ctx.borrow(), clause, proof)
}
ProofStep::UnitClauses { units } => check_unit_clauses_step(ctx.borrow(), units),
ProofStep::ChangeHashBits { bits } => {
ctx.part_mut(ClauseHasherP).hash_bits = bits;
rehash(ctx.borrow());
Ok(())
}
ProofStep::Model { assignment } => check_model_step(ctx.borrow(), assignment),
ProofStep::Assumptions { assumptions } => {
for &lit in assumptions.iter() {
ensure_sampling_var(ctx.borrow(), lit.var())?;
}
copy_canonical(&mut ctx.part_mut(CheckerStateP).assumptions, assumptions);
let (state, mut ctx) = ctx.split_part(CheckerStateP);
process_step(
ctx.borrow(),
&CheckedProofStep::Assumptions {
assumptions: &state.assumptions,
},
)?;
Ok(())
}
ProofStep::FailedAssumptions {
failed_core,
propagation_hashes,
} => check_failed_assumptions_step(ctx.borrow(), failed_core, propagation_hashes),
ProofStep::End => {
ctx.part_mut(CheckerStateP).ended = true;
Ok(())
}
};
if let Err(CheckerError::CheckFailed {
ref mut debug_step, ..
}) = result
{
*debug_step = format!("{:?}", step)
}
result
}
/// Check a DeleteVar step
fn check_delete_var_step<'a>(
mut ctx: partial!(
Context<'a>,
mut ClausesP,
mut ProcessingP<'a>,
mut VariablesP,
CheckerStateP,
),
var: Var,
) -> Result<(), CheckerError> {
ensure_var(ctx.borrow(), var);
if let Some(user_var) = ctx.part(VariablesP).var_data[var.index()].user_var {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!(
"deleted variable {:?} corresponds to user variable {:?}",
var, user_var
),
));
}
for &polarity in &[false, true] {
if ctx.part(VariablesP).lit_data[var.lit(polarity).code()].clause_count > 0 {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!("deleted variable {:?} still has clauses", var),
));
}
}
if let Some(unit_clause) = ctx.part(ClausesP).unit_clauses[var.index()] {
let clause = [var.lit(unit_clause.value)];
let id = match unit_clause.id {
UnitId::Global(id) => id,
_ => unreachable!(),
};
process_step(
ctx.borrow(),
&CheckedProofStep::DeleteRatClause {
id,
keep_as_redundant: false,
clause: &clause[..],
pivot: clause[0],
propagations: &ResolutionPropagations {},
},
)?;
ctx.part_mut(ClausesP).unit_clauses[var.index()] = None;
}
ctx.part_mut(VariablesP).var_data[var.index()] = VarData::default();
Ok(())
}
/// Check a ChangeSamplingMode step
fn check_change_sampling_mode<'a>(
mut ctx: partial!(
Context<'a>,
mut ClausesP,
mut ProcessingP<'a>,
mut VariablesP,
CheckerStateP,
),
var: Var,
sample: bool,
) -> Result<(), CheckerError> {
ensure_var(ctx.borrow(), var);
let mut ctx_in = ctx;
let (variables, ctx) = ctx_in.split_part_mut(VariablesP);
let var_data = &mut variables.var_data[var.index()];
let sampling_mode = if sample {
SamplingMode::Sample
} else {
SamplingMode::Witness
};
if var_data.sampling_mode != sampling_mode {
var_data.sampling_mode = sampling_mode;
if let Some(user_var) = var_data.user_var {
let sampling_mode = if var_data.sampling_mode == SamplingMode::Witness {
CheckedSamplingMode::Witness
} else {
CheckedSamplingMode::Sample
};
process_step(
ctx_in.borrow(),
&CheckedProofStep::UserVar {
var,
user_var: Some(CheckedUserVar {
user_var,
sampling_mode,
new_var: false,
}),
},
)?;
} else if sampling_mode == SamplingMode::Sample {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!("cannot sample hidden variable {:?}", var),
));
}
}
Ok(())
}
/// Check an AtClause step
fn check_at_clause_step<'a>(
mut ctx: partial!(
Context<'a>,
mut CheckerStateP,
mut ClauseHasherP,
mut ClausesP,
mut ProcessingP<'a>,
mut RupCheckP,
mut TmpDataP,
mut VariablesP,
),
redundant: bool,
clause: &[Lit],
propagation_hashes: &[ClauseHash],
) -> Result<(), CheckerError> {
let mut tmp = replace(&mut ctx.part_mut(TmpDataP).tmp, vec![]);
if copy_canonical(&mut tmp, clause) {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!("clause {:?} is a tautology", tmp),
));
}
check_clause_with_hashes(ctx.borrow(), &tmp, &*propagation_hashes)?;
let (id, added) = store_clause(ctx.borrow(), &tmp, redundant);
if !redundant {
let state = ctx.part_mut(CheckerStateP);
state.previous_irred_clause_id = Some(id);
state.previous_irred_clause_lits.clear();
state.previous_irred_clause_lits.extend_from_slice(&tmp);
}
match added {
StoreClauseResult::New => {
let (rup_check, mut ctx) = ctx.split_part(RupCheckP);
process_step(
ctx.borrow(),
&CheckedProofStep::AtClause {
id,
redundant,
clause: &tmp,
propagations: &rup_check.trace_ids,
},
)?;
}
StoreClauseResult::NewlyIrredundant => {
process_step(
ctx.borrow(),
&CheckedProofStep::MakeIrredundant { id, clause: &tmp },
)?;
}
StoreClauseResult::Duplicate => (),
}
ctx.part_mut(TmpDataP).tmp = tmp;
Ok(())
}
/// Check a DeleteClause step
fn check_delete_clause_step<'a>(
mut ctx: partial!(
Context<'a>,
mut CheckerStateP,
mut ClausesP,
mut ProcessingP<'a>,
mut TmpDataP,
mut VariablesP,
ClauseHasherP,
),
clause: &[Lit],
proof: DeleteClauseProof,
) -> Result<(), CheckerError> {
let mut tmp = replace(&mut ctx.part_mut(TmpDataP).tmp, vec![]);
if copy_canonical(&mut tmp, clause) {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!("clause {:?} is a tautology", tmp),
));
}
let redundant = proof == DeleteClauseProof::Redundant;
let mut subsumed_by = None;
match proof {
DeleteClauseProof::Redundant => (),
DeleteClauseProof::Satisfied => {
let is_subsumed = tmp.iter().any(|&lit| {
if let Some((
true,
UnitClause {
id: UnitId::Global(id),
..
},
)) = ctx.part(ClausesP).lit_value(lit)
{
subsumed_by = Some(id);
true
} else {
false
}
});
if !is_subsumed {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!("deleted clause {:?} is not satisfied", clause),
));
}
}
DeleteClauseProof::Simplified => {
subsumed_by = ctx.part(CheckerStateP).previous_irred_clause_id;
if !ctx
.part(CheckerStateP)
.subsumed_by_previous_irred_clause(&tmp)
{
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!(
"deleted clause {:?} is not subsumed by previous clause {:?}",
clause,
ctx.part(CheckerStateP).previous_irred_clause_lits
),
));
}
}
}
ctx.part_mut(CheckerStateP).previous_irred_clause_id = None;
ctx.part_mut(CheckerStateP)
.previous_irred_clause_lits
.clear();
let (id, deleted) = delete_clause(ctx.borrow(), &tmp, redundant)?;
if redundant {
match deleted {
DeleteClauseResult::Removed => {
process_step(
ctx.borrow(),
&CheckedProofStep::DeleteClause { id, clause: &tmp },
)?;
}
DeleteClauseResult::Unchanged => (),
DeleteClauseResult::NewlyRedundant => unreachable!(),
}
} else {
match deleted {
DeleteClauseResult::Removed | DeleteClauseResult::NewlyRedundant => {
process_step(
ctx.borrow(),
&CheckedProofStep::DeleteAtClause {
id,
keep_as_redundant: deleted == DeleteClauseResult::NewlyRedundant,
clause: &tmp,
propagations: &[subsumed_by.unwrap()],
},
)?;
}
DeleteClauseResult::Unchanged => (),
}
}
ctx.part_mut(TmpDataP).tmp = tmp;
Ok(())
}
/// Check a UnitClauses step
fn check_unit_clauses_step<'a>(
mut ctx: partial!(
Context<'a>,
mut CheckerStateP,
mut ClauseHasherP,
mut ClausesP,
mut ProcessingP<'a>,
mut RupCheckP,
mut VariablesP,
),
units: &[(Lit, ClauseHash)],
) -> Result<(), CheckerError> {
for &(lit, hash) in units.iter() {
ensure_var(ctx.borrow(), lit.var());
let clause = [lit];
let propagation_hashes = [hash];
check_clause_with_hashes(ctx.borrow(), &clause[..], &propagation_hashes[..])?;
let (id, added) = store_unit_clause(ctx.borrow(), lit);
match added {
StoreClauseResult::New => {
let (rup_check, mut ctx) = ctx.split_part(RupCheckP);
process_step(
ctx.borrow(),
&CheckedProofStep::AtClause {
id,
redundant: false,
clause: &clause,
propagations: &rup_check.trace_ids,
},
)?;
}
StoreClauseResult::Duplicate => (),
StoreClauseResult::NewlyIrredundant => unreachable!(),
}
}
Ok(())
}
/// Check a Model step
fn check_model_step<'a>(
mut ctx: partial!(Context<'a>, mut ProcessingP<'a>, CheckerStateP, ClausesP, VariablesP),
model: &[Lit],
) -> Result<(), CheckerError> {
let mut assignments = HashSet::default();
for &lit in model.iter() {
if let Some((false, _)) = ctx.part(ClausesP).lit_value(lit) {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!("model assignment conflicts with unit clause {:?}", !lit),
));
}
if assignments.contains(&!lit) {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!("model contains conflicting assignment {:?}", !lit),
));
}
assignments.insert(lit);
}
for &lit in ctx.part(CheckerStateP).assumptions.iter() {
if !assignments.contains(&lit) {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!("model does not contain assumption {:?}", lit),
));
}
}
for (_, candidates) in ctx.part(ClausesP).clauses.iter() {
for clause in candidates.iter() {
let lits = clause.lits.slice(&ctx.part(ClausesP).literal_buffer);
if !lits.iter().any(|lit| assignments.contains(&lit)) {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
format!("model does not satisfy clause {:?}", lits),
));
}
}
}
process_step(ctx.borrow(), &CheckedProofStep::Model { assignment: model })?;
Ok(())
}
/// Check a FailedAssumptions step
fn check_failed_assumptions_step<'a>(
mut ctx: partial!(
Context<'a>,
mut ClauseHasherP,
mut ClausesP,
mut ProcessingP<'a>,
mut RupCheckP,
mut TmpDataP,
mut VariablesP,
CheckerStateP,
),
failed_core: &[Lit],
propagation_hashes: &[ClauseHash],
) -> Result<(), CheckerError> {
let mut tmp = replace(&mut ctx.part_mut(TmpDataP).tmp, vec![]);
let direct_conflict = copy_canonical(&mut tmp, failed_core);
if !is_subset(&tmp, &ctx.part(CheckerStateP).assumptions, false) {
return Err(CheckerError::check_failed(
ctx.part(CheckerStateP).step,
"failed core contains non-assumed variables".to_string(),
));
}
if direct_conflict {
// we have x and not x among the assumptions, no need to check
ctx.part_mut(RupCheckP).trace_ids.clear();
} else {
// invert the assumptions and check for an AT
for lit in tmp.iter_mut() {
*lit = !*lit;
}
check_clause_with_hashes(ctx.borrow(), &tmp, propagation_hashes)?;
// we undo the inversion to report the correct checked proof step
for lit in tmp.iter_mut() {
*lit = !*lit;
}
}
let (rup_check, mut ctx) = ctx.split_part(RupCheckP);
process_step(
ctx.borrow(),
&CheckedProofStep::FailedAssumptions {
failed_core: &tmp,
propagations: &rup_check.trace_ids,
},
)?;
ctx.part_mut(TmpDataP).tmp = tmp;
Ok(())
}
/// Checks a proof in the native Varisat format.
pub fn check_proof<'a>(
mut ctx: partial!(
Context<'a>,
mut CheckerStateP,
mut ClauseHasherP,
mut ClausesP,
mut ProcessingP<'a>,
mut RupCheckP,
mut TmpDataP,
mut VariablesP,
),
input: impl io::Read,
) -> Result<(), CheckerError> {
let mut buffer = io::BufReader::new(input);
let mut parser = Parser::default();
while !ctx.part(CheckerStateP).ended {
ctx.part_mut(CheckerStateP).step += 1;
let step = ctx.part(CheckerStateP).step;
if step % 100000 == 0 {
log::info!("checking step {}k", step / 1000);
}
match parser.parse_step(&mut buffer) {
Ok(step) => check_step(ctx.borrow(), step)?,
Err(err) => match err.downcast::<io::Error>() {
Ok(io_err) => {
if io_err.kind() == io::ErrorKind::UnexpectedEof {
return Err(CheckerError::ProofIncomplete { step });
} else {
return Err(CheckerError::IoError {
step,
cause: io_err,
});
}
}
Err(err) => return Err(CheckerError::ParseError { step, cause: err }),
},
}
}
process_unit_conflicts(ctx.borrow())
}
/// Process unit conflicts detected during clause loading.
pub fn process_unit_conflicts<'a>(
mut ctx: partial!(Context<'a>, mut ProcessingP<'a>, ClausesP, VariablesP),
) -> Result<(), CheckerError> {
let (clauses, mut ctx) = ctx.split_part(ClausesP);
if let Some(ids) = &clauses.unit_conflict {
let clause = &[];
process_step(
ctx.borrow(),
&CheckedProofStep::AtClause {
id: clauses.next_clause_id,
redundant: false,
clause,
propagations: ids,
},
)?;
}
Ok(())
}