| //! Clause storage (unit and non-unit clauses). |
| use std::{convert::TryInto, mem::transmute}; |
| |
| use partial_ref::{partial, PartialRef}; |
| use rustc_hash::FxHashMap as HashMap; |
| use smallvec::SmallVec; |
| |
| use varisat_formula::{lit::LitIdx, Lit}; |
| use varisat_internal_proof::ClauseHash; |
| |
| use crate::{ |
| context::{parts::*, Context}, |
| processing::{process_step, CheckedProofStep}, |
| sorted_lits::copy_canonical, |
| variables::{ensure_sampling_var, ensure_var}, |
| CheckerError, |
| }; |
| |
| const INLINE_LITS: usize = 3; |
| |
| /// Literals of a clause, either inline or an index into a buffer |
| pub struct ClauseLits { |
| length: LitIdx, |
| inline: [LitIdx; INLINE_LITS], |
| } |
| |
| impl ClauseLits { |
| /// Create a new ClauseLits, storing them in the given buffer if necessary |
| fn new(lits: &[Lit], buffer: &mut Vec<Lit>) -> ClauseLits { |
| let mut inline = [0; INLINE_LITS]; |
| let length = lits.len(); |
| |
| if length > INLINE_LITS { |
| inline[0] = buffer |
| .len() |
| .try_into() |
| .expect("exceeded maximal literal buffer size"); |
| buffer.extend(lits); |
| } else { |
| let lits = unsafe { |
| // Lit is a repr(transparent) wrapper of LitIdx |
| #[allow(clippy::transmute_ptr_to_ptr)] |
| transmute::<&[Lit], &[LitIdx]>(lits) |
| }; |
| inline[..length].copy_from_slice(lits); |
| } |
| |
| ClauseLits { |
| length: length as LitIdx, |
| inline, |
| } |
| } |
| |
| /// Returns the literals as a slice given a storage buffer |
| pub fn slice<'a, 'b, 'c>(&'a self, buffer: &'b [Lit]) -> &'c [Lit] |
| where |
| 'a: 'c, |
| 'b: 'c, |
| { |
| if self.length > INLINE_LITS as LitIdx { |
| &buffer[self.inline[0] as usize..][..self.length as usize] |
| } else { |
| unsafe { |
| // Lit is a repr(transparent) wrapper of LitIdx |
| #[allow(clippy::transmute_ptr_to_ptr)] |
| transmute::<&[LitIdx], &[Lit]>(&self.inline[..self.length as usize]) |
| } |
| } |
| } |
| |
| /// Literals stored in the literal buffer |
| fn buffer_used(&self) -> usize { |
| if self.length > INLINE_LITS as LitIdx { |
| self.length as usize |
| } else { |
| 0 |
| } |
| } |
| } |
| |
| /// Literals and metadata for non-unit clauses. |
| pub struct Clause { |
| /// LRAT clause id. |
| pub id: u64, |
| /// How often the clause is present as irred., red. clause. |
| /// |
| /// For checking the formula is a multiset of clauses. This is necessary as the generating |
| /// solver might not check for duplicated clauses. |
| ref_count: [u32; 2], |
| /// Clause's literals. |
| pub lits: ClauseLits, |
| } |
| |
| /// Identifies the origin of a unit clause. |
| #[derive(Copy, Clone, Debug)] |
| pub enum UnitId { |
| Global(u64), |
| TracePos(usize), |
| InClause, |
| } |
| |
| /// Known unit clauses and metadata. |
| #[derive(Copy, Clone, Debug)] |
| pub struct UnitClause { |
| pub id: UnitId, |
| pub value: bool, |
| } |
| |
| /// Return type of [`store_clause`] |
| #[derive(Copy, Clone, PartialEq, Eq)] |
| pub enum StoreClauseResult { |
| New, |
| Duplicate, |
| NewlyIrredundant, |
| } |
| |
| /// Return type of [`delete_clause`] |
| #[derive(Copy, Clone, PartialEq, Eq)] |
| pub enum DeleteClauseResult { |
| Unchanged, |
| NewlyRedundant, |
| Removed, |
| } |
| /// Checker clause storage. |
| #[derive(Default)] |
| pub struct Clauses { |
| /// Next clause id to use. |
| pub next_clause_id: u64, |
| /// Literal storage for clauses, |
| pub literal_buffer: Vec<Lit>, |
| /// Number of literals in the buffer which are from deleted clauses. |
| garbage_size: usize, |
| /// Stores all known non-unit clauses indexed by their hash. |
| pub clauses: HashMap<ClauseHash, SmallVec<[Clause; 1]>>, |
| /// Stores known unit clauses and propagations during a clause check. |
| pub unit_clauses: Vec<Option<UnitClause>>, |
| /// This stores a conflict of input unit clauses. |
| /// |
| /// Our representation for unit clauses doesn't support conflicting units so this is used as a |
| /// workaround. |
| pub unit_conflict: Option<[u64; 2]>, |
| } |
| |
| impl Clauses { |
| /// Value of a literal if known from unit clauses. |
| pub fn lit_value(&self, lit: Lit) -> Option<(bool, UnitClause)> { |
| self.unit_clauses[lit.index()] |
| .map(|unit_clause| (unit_clause.value ^ lit.is_negative(), unit_clause)) |
| } |
| } |
| |
| /// Adds a clause to the checker. |
| pub fn add_clause<'a>( |
| mut ctx: partial!(Context<'a>, mut ClausesP, mut CheckerStateP, mut ProcessingP<'a>, mut TmpDataP, mut VariablesP, ClauseHasherP), |
| clause: &[Lit], |
| ) -> Result<(), CheckerError> { |
| if ctx.part(CheckerStateP).unsat { |
| return Ok(()); |
| } |
| |
| let (tmp_data, mut ctx) = ctx.split_part_mut(TmpDataP); |
| |
| if copy_canonical(&mut tmp_data.tmp, clause) { |
| let (clauses, mut ctx) = ctx.split_part_mut(ClausesP); |
| process_step( |
| ctx.borrow(), |
| &CheckedProofStep::TautologicalClause { |
| id: clauses.next_clause_id, |
| clause: &tmp_data.tmp, |
| }, |
| )?; |
| clauses.next_clause_id += 1; |
| return Ok(()); |
| } |
| |
| for &lit in tmp_data.tmp.iter() { |
| ensure_sampling_var(ctx.borrow(), lit.var())?; |
| } |
| |
| let (id, added) = store_clause(ctx.borrow(), &tmp_data.tmp, false); |
| |
| let (clauses, mut ctx) = ctx.split_part_mut(ClausesP); |
| |
| match added { |
| StoreClauseResult::New => { |
| process_step( |
| ctx.borrow(), |
| &CheckedProofStep::AddClause { |
| id, |
| clause: &tmp_data.tmp, |
| }, |
| )?; |
| } |
| StoreClauseResult::NewlyIrredundant | StoreClauseResult::Duplicate => { |
| if let StoreClauseResult::NewlyIrredundant = added { |
| process_step( |
| ctx.borrow(), |
| &CheckedProofStep::MakeIrredundant { |
| id, |
| clause: &tmp_data.tmp, |
| }, |
| )?; |
| } |
| |
| process_step( |
| ctx.borrow(), |
| &CheckedProofStep::DuplicatedClause { |
| id: clauses.next_clause_id, |
| same_as_id: id, |
| clause: &tmp_data.tmp, |
| }, |
| )?; |
| // This is a duplicated clause. We want to ensure that the clause ids match the input |
| // order so we skip a clause id. |
| clauses.next_clause_id += 1; |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| /// Adds a clause to the checker data structures. |
| /// |
| /// `lits` must be sorted and free of duplicates. |
| /// |
| /// Returns the id of the added clause and indicates whether the clause is new or changed from |
| /// redundant to irredundant. |
| pub fn store_clause( |
| mut ctx: partial!( |
| Context, |
| mut CheckerStateP, |
| mut ClausesP, |
| mut VariablesP, |
| ClauseHasherP |
| ), |
| lits: &[Lit], |
| redundant: bool, |
| ) -> (u64, StoreClauseResult) { |
| for &lit in lits.iter() { |
| ensure_var(ctx.borrow(), lit.var()); |
| } |
| |
| match lits[..] { |
| [] => { |
| let id = ctx.part(ClausesP).next_clause_id; |
| ctx.part_mut(ClausesP).next_clause_id += 1; |
| |
| ctx.part_mut(CheckerStateP).unsat = true; |
| (id, StoreClauseResult::New) |
| } |
| [lit] => store_unit_clause(ctx.borrow(), lit), |
| _ => { |
| let hash = ctx.part(ClauseHasherP).clause_hash(&lits); |
| |
| let (clauses, mut ctx) = ctx.split_part_mut(ClausesP); |
| |
| let candidates = clauses.clauses.entry(hash).or_default(); |
| |
| for candidate in candidates.iter_mut() { |
| if candidate.lits.slice(&clauses.literal_buffer) == &lits[..] { |
| let result = if !redundant && candidate.ref_count[0] == 0 { |
| // first irredundant copy |
| StoreClauseResult::NewlyIrredundant |
| } else { |
| StoreClauseResult::Duplicate |
| }; |
| |
| let ref_count = &mut candidate.ref_count[redundant as usize]; |
| *ref_count = ref_count.checked_add(1).expect("ref_count overflow"); |
| return (candidate.id, result); |
| } |
| } |
| |
| let id = clauses.next_clause_id; |
| |
| let mut ref_count = [0, 0]; |
| ref_count[redundant as usize] += 1; |
| |
| candidates.push(Clause { |
| id, |
| ref_count, |
| lits: ClauseLits::new(&lits, &mut clauses.literal_buffer), |
| }); |
| |
| clauses.next_clause_id += 1; |
| |
| for &lit in lits.iter() { |
| ctx.part_mut(VariablesP).lit_data[lit.code()].clause_count += 1; |
| } |
| |
| (id, StoreClauseResult::New) |
| } |
| } |
| } |
| |
| /// Adds a unit clause to the checker data structures. |
| /// |
| /// Returns the id of the added clause and a boolean that is true if the clause wasn't already |
| /// present. |
| pub fn store_unit_clause( |
| mut ctx: partial!(Context, mut CheckerStateP, mut ClausesP), |
| lit: Lit, |
| ) -> (u64, StoreClauseResult) { |
| match ctx.part(ClausesP).lit_value(lit) { |
| Some(( |
| true, |
| UnitClause { |
| id: UnitId::Global(id), |
| .. |
| }, |
| )) => (id, StoreClauseResult::Duplicate), |
| Some(( |
| false, |
| UnitClause { |
| id: UnitId::Global(conflicting_id), |
| .. |
| }, |
| )) => { |
| ctx.part_mut(CheckerStateP).unsat = true; |
| let id = ctx.part(ClausesP).next_clause_id; |
| ctx.part_mut(ClausesP).unit_conflict = Some([conflicting_id, id]); |
| ctx.part_mut(ClausesP).next_clause_id += 1; |
| (id, StoreClauseResult::New) |
| } |
| Some(_) => unreachable!(), |
| None => { |
| let id = ctx.part(ClausesP).next_clause_id; |
| |
| ctx.part_mut(ClausesP).unit_clauses[lit.index()] = Some(UnitClause { |
| value: lit.is_positive(), |
| id: UnitId::Global(id), |
| }); |
| |
| ctx.part_mut(ClausesP).next_clause_id += 1; |
| |
| (id, StoreClauseResult::New) |
| } |
| } |
| } |
| |
| /// Delete a clause from the current formula. |
| /// |
| /// `lits` must be sorted and free of duplicates. |
| /// |
| /// Returns the id of the deleted clause and whether the ref_count (or irredundant ref_count) |
| /// became zero. |
| pub fn delete_clause( |
| mut ctx: partial!( |
| Context, |
| mut ClausesP, |
| mut VariablesP, |
| CheckerStateP, |
| ClauseHasherP |
| ), |
| lits: &[Lit], |
| redundant: bool, |
| ) -> Result<(u64, DeleteClauseResult), CheckerError> { |
| if lits.len() < 2 { |
| return Err(CheckerError::check_failed( |
| ctx.part(CheckerStateP).step, |
| format!("delete of unit or empty clause {:?}", lits), |
| )); |
| } |
| |
| let hash = ctx.part(ClauseHasherP).clause_hash(lits); |
| |
| let clauses = ctx.part_mut(ClausesP); |
| |
| let candidates = clauses.clauses.entry(hash).or_default(); |
| |
| let mut found = false; |
| |
| let mut result = None; |
| |
| let literal_buffer = &clauses.literal_buffer; |
| let garbage_size = &mut clauses.garbage_size; |
| |
| candidates.retain(|candidate| { |
| if found || candidate.lits.slice(literal_buffer) != lits { |
| true |
| } else { |
| found = true; |
| let ref_count = &mut candidate.ref_count[redundant as usize]; |
| |
| if *ref_count == 0 { |
| true |
| } else { |
| *ref_count -= 1; |
| |
| if candidate.ref_count == [0, 0] { |
| *garbage_size += candidate.lits.buffer_used(); |
| result = Some((candidate.id, DeleteClauseResult::Removed)); |
| false |
| } else { |
| if !redundant && candidate.ref_count[0] == 0 { |
| result = Some((candidate.id, DeleteClauseResult::NewlyRedundant)); |
| } else { |
| result = Some((candidate.id, DeleteClauseResult::Unchanged)); |
| } |
| true |
| } |
| } |
| } |
| }); |
| |
| if candidates.is_empty() { |
| clauses.clauses.remove(&hash); |
| } |
| |
| if let Some((_, DeleteClauseResult::Removed)) = result { |
| for &lit in lits.iter() { |
| ctx.part_mut(VariablesP).lit_data[lit.code()].clause_count -= 1; |
| } |
| } |
| |
| if let Some(result) = result { |
| collect_garbage(ctx.borrow()); |
| return Ok(result); |
| } |
| |
| let msg = match (found, redundant) { |
| (false, _) => format!("delete of unknown clause {:?}", lits), |
| (_, true) => format!("delete of redundant clause {:?} which is irredundant", lits), |
| (_, false) => format!("delete of irredundant clause {:?} which is redundant", lits), |
| }; |
| Err(CheckerError::check_failed( |
| ctx.part(CheckerStateP).step, |
| msg, |
| )) |
| } |
| |
| /// Perform a garbage collection if required |
| fn collect_garbage(mut ctx: partial!(Context, mut ClausesP)) { |
| let clauses = ctx.part_mut(ClausesP); |
| if clauses.garbage_size * 2 <= clauses.literal_buffer.len() { |
| return; |
| } |
| |
| let mut new_buffer = vec![]; |
| |
| new_buffer.reserve(clauses.literal_buffer.len()); |
| |
| for (_, candidates) in clauses.clauses.iter_mut() { |
| for clause in candidates.iter_mut() { |
| let new_lits = |
| ClauseLits::new(clause.lits.slice(&clauses.literal_buffer), &mut new_buffer); |
| clause.lits = new_lits; |
| } |
| } |
| |
| clauses.literal_buffer = new_buffer; |
| clauses.garbage_size = 0; |
| } |