//! Unit propagation. use partial_ref::{partial, PartialRef}; use crate::context::{parts::*, Context}; pub mod assignment; pub mod binary; pub mod graph; pub mod long; pub mod watch; pub use assignment::{backtrack, enqueue_assignment, full_restart, restart, Assignment, Trail}; pub use graph::{Conflict, ImplGraph, ImplNode, Reason}; pub use watch::{enable_watchlists, Watch, Watchlists}; /// Propagate enqueued assignments. /// /// Returns when all enqueued assignments are propagated, including newly propagated assignemnts, or /// if there is a conflict. /// /// On conflict the first propagation that would assign the opposite value to an already assigned /// literal is returned. pub fn propagate( mut ctx: partial!( Context, mut AssignmentP, mut ClauseAllocP, mut ImplGraphP, mut TrailP, mut WatchlistsP, BinaryClausesP, ClauseDbP, ), ) -> Result<(), Conflict> { enable_watchlists(ctx.borrow()); while let Some(lit) = ctx.part_mut(TrailP).pop_queue() { binary::propagate_binary(ctx.borrow(), lit)?; long::propagate_long(ctx.borrow(), lit)?; } Ok(()) } #[cfg(test)] mod tests { use super::*; use proptest::{prelude::*, *}; use rand::{distributions::Bernoulli, seq::SliceRandom}; use partial_ref::IntoPartialRefMut; use varisat_formula::{cnf::strategy::*, CnfFormula, Lit}; use crate::{ clause::{db, gc}, load::load_clause, state::SatState, }; /// Generate a random formula and list of implied literals. pub fn prop_formula( vars: impl Strategy, extra_vars: impl Strategy, extra_clauses: impl Strategy, density: impl Strategy, ) -> impl Strategy, CnfFormula)> { (vars, extra_vars, extra_clauses, density).prop_flat_map( |(vars, extra_vars, extra_clauses, density)| { let polarity = collection::vec(bool::ANY, vars + extra_vars); let dist = Bernoulli::new(density).unwrap(); let lits = polarity .prop_map(|polarity| { polarity .into_iter() .enumerate() .map(|(index, polarity)| Lit::from_index(index, polarity)) .collect::>() }) .prop_shuffle(); lits.prop_perturb(move |mut lits, mut rng| { let assigned_lits = &lits[..vars]; let mut clauses: Vec> = vec![]; for (i, &lit) in assigned_lits.iter().enumerate() { // Build a clause that implies lit let mut clause = vec![lit]; for &reason_lit in assigned_lits[..i].iter() { if rng.sample(dist) { clause.push(!reason_lit); } } clause.shuffle(&mut rng); clauses.push(clause); } for _ in 0..extra_clauses { // Build a clause that is satisfied let &true_lit = assigned_lits.choose(&mut rng).unwrap(); let mut clause = vec![true_lit]; for &other_lit in lits.iter() { if other_lit != true_lit && rng.sample(dist) { clause.push(other_lit ^ rng.gen::()); } } clause.shuffle(&mut rng); clauses.push(clause); } clauses.shuffle(&mut rng); // Only return implied lits lits.drain(vars..); (lits, CnfFormula::from(clauses)) }) }, ) } proptest! { #[test] fn propagation_no_conflict( (mut lits, formula) in prop_formula( 2..30usize, 0..10usize, 0..20usize, 0.1..0.9 ), ) { let mut ctx = Context::default(); let mut ctx = ctx.into_partial_ref_mut(); for clause in formula.iter() { load_clause(ctx.borrow(), clause); } prop_assert_eq!(ctx.part(SolverStateP).sat_state, SatState::Unknown); let prop_result = propagate(ctx.borrow()); prop_assert_eq!(prop_result, Ok(())); lits.sort(); let mut prop_lits = ctx.part(TrailP).trail().to_owned(); // Remap vars for lit in prop_lits.iter_mut() { *lit = lit.map_var(|solver_var| { ctx.part(VariablesP).existing_user_from_solver(solver_var) }); } prop_lits.sort(); prop_assert_eq!(prop_lits, lits); } #[test] fn propagation_conflict( (lits, formula) in prop_formula( 2..30usize, 0..10usize, 0..20usize, 0.1..0.9 ), conflict_size in any::(), ) { let mut ctx = Context::default(); let mut ctx = ctx.into_partial_ref_mut(); // We add the conflict clause first to make sure that it isn't simplified during loading let conflict_size = conflict_size.index(lits.len() - 1) + 2; let conflict_clause: Vec<_> = lits[..conflict_size].iter().map(|&lit| !lit).collect(); load_clause(ctx.borrow(), &conflict_clause); for clause in formula.iter() { load_clause(ctx.borrow(), clause); } prop_assert_eq!(ctx.part(SolverStateP).sat_state, SatState::Unknown); let prop_result = propagate(ctx.borrow()); prop_assert!(prop_result.is_err()); let conflict = prop_result.unwrap_err(); let conflict_lits = conflict.lits(&ctx.borrow()).to_owned(); for &lit in conflict_lits.iter() { prop_assert!(ctx.part(AssignmentP).lit_is_false(lit)); } } #[test] fn propagation_no_conflict_after_gc( tmp_formula in cnf_formula(3..30usize, 30..100, 3..30), (mut lits, formula) in prop_formula( 2..30usize, 0..10usize, 0..20usize, 0.1..0.9 ), ) { let mut ctx = Context::default(); let mut ctx = ctx.into_partial_ref_mut(); for clause in tmp_formula.iter() { // Only add long clauses here let mut lits = clause.to_owned(); lits.sort(); lits.dedup(); if lits.len() >= 3 { load_clause(ctx.borrow(), clause); } } let tmp_crefs: Vec<_> = db::clauses_iter(&ctx.borrow()).collect(); for clause in formula.iter() { load_clause(ctx.borrow(), clause); } for cref in tmp_crefs { db::delete_clause(ctx.borrow(), cref); } gc::collect_garbage(ctx.borrow()); prop_assert_eq!(ctx.part(SolverStateP).sat_state, SatState::Unknown); let prop_result = propagate(ctx.borrow()); prop_assert_eq!(prop_result, Ok(())); lits.sort(); let mut prop_lits = ctx.part(TrailP).trail().to_owned(); // Remap vars for lit in prop_lits.iter_mut() { *lit = lit.map_var(|solver_var| { ctx.part(VariablesP).existing_user_from_solver(solver_var) }); } prop_lits.sort(); prop_assert_eq!(prop_lits, lits); } } }