| //! 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(()) |
| } |