//! 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) -> 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, /// 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>, /// Stores known unit clauses and propagations during a clause check. pub unit_clauses: Vec>, /// 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; }