//! 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, /// Last added irredundant clause literals. previous_irred_clause_lits: Vec, /// Current assumptions, used to check FailedAssumptions and Model assumptions: Vec, } 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::() { 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(()) }