varisat_lrat/
lib.rs

1//! LRAT proof generation for the Varisat SAT solver.
2use std::{
3    io::{BufWriter, Write},
4    mem::replace,
5};
6
7use anyhow::Error;
8
9use varisat_checker::{CheckedProofStep, CheckerData, ProofProcessor};
10use varisat_formula::Lit;
11
12/// Proof processor that generates an LRAT proof.
13pub struct WriteLrat<'a> {
14    binary: bool,
15    target: BufWriter<Box<dyn Write + 'a>>,
16    delete_open: bool,
17    last_added_id: u64,
18    buffered_deletes: Vec<u64>,
19}
20
21impl<'a> ProofProcessor for WriteLrat<'a> {
22    fn process_step(&mut self, step: &CheckedProofStep, _data: CheckerData) -> Result<(), Error> {
23        match step {
24            CheckedProofStep::AddClause { .. } => (),
25            CheckedProofStep::DuplicatedClause { .. } => (),
26            _ => {
27                if !self.buffered_deletes.is_empty() {
28                    let buffered_deletes = replace(&mut self.buffered_deletes, vec![]);
29                    self.open_delete()?;
30                    self.write_ids(&buffered_deletes)?;
31                }
32            }
33        }
34
35        match step {
36            &CheckedProofStep::AddClause { id, .. } => {
37                self.last_added_id = id;
38            }
39            &CheckedProofStep::DuplicatedClause { id, .. }
40            | &CheckedProofStep::TautologicalClause { id, .. } => {
41                self.last_added_id = id;
42                if self.binary {
43                    self.open_delete()?;
44                    self.write_ids(&[id])?;
45                } else {
46                    // In the textual format the delete command is prefixed by an id which we do not
47                    // know yet.
48                    self.buffered_deletes.push(id);
49                }
50            }
51            &CheckedProofStep::AtClause {
52                id,
53                clause,
54                propagations,
55                ..
56            } => {
57                self.close_delete()?;
58                self.last_added_id = id;
59                self.write_add_step()?;
60                self.write_ids(&[id])?;
61                self.write_lits(clause)?;
62                self.write_sep()?;
63                self.write_ids(propagations)?;
64                self.write_end()?;
65            }
66            &CheckedProofStep::DeleteAtClause {
67                id,
68                keep_as_redundant,
69                ..
70            }
71            | &CheckedProofStep::DeleteRatClause {
72                id,
73                keep_as_redundant,
74                ..
75            } => {
76                if !keep_as_redundant {
77                    self.open_delete()?;
78                    self.write_ids(&[id])?;
79                }
80            }
81            &CheckedProofStep::DeleteClause { id, .. } => {
82                self.open_delete()?;
83                self.write_ids(&[id])?;
84            }
85            &CheckedProofStep::UserVar { .. }
86            | &CheckedProofStep::MakeIrredundant { .. }
87            | &CheckedProofStep::Model { .. }
88            | &CheckedProofStep::Assumptions { .. }
89            | &CheckedProofStep::FailedAssumptions { .. } => (),
90        }
91        Ok(())
92    }
93}
94
95impl<'a> WriteLrat<'a> {
96    /// Create a lrat writing processor.
97    ///
98    /// The proof is written to `target`. If `binary` is false a normal LRAT proof is emitted. If it
99    /// is true, the compressed LRAT format is used which is a compact binary encoding. Despite the
100    /// name, even a compressed LRAT proof can usually still be compressed a lot using a general
101    /// data compression algorithm.
102    pub fn new(target: impl Write + 'a, binary: bool) -> WriteLrat<'a> {
103        WriteLrat {
104            binary,
105            target: BufWriter::new(Box::new(target)),
106            delete_open: false,
107            last_added_id: 0,
108            buffered_deletes: vec![],
109        }
110    }
111
112    /// Write out all steps processed so far.
113    ///
114    /// This is automatically called when this proof processor is dropped. Calling this explicitly
115    /// is recommended to handle possible IO errors.
116    pub fn flush(&mut self) -> Result<(), Error> {
117        self.close_delete()?;
118        self.target.flush()?;
119        Ok(())
120    }
121
122    /// If necessary begin a batched delete step.
123    fn open_delete(&mut self) -> Result<(), Error> {
124        if !self.delete_open {
125            if !self.binary {
126                self.write_ids(&[self.last_added_id])?;
127            }
128            self.write_delete_step()?;
129            self.delete_open = true;
130        }
131        Ok(())
132    }
133
134    /// If necessary end a batched delete step.
135    fn close_delete(&mut self) -> Result<(), Error> {
136        if self.delete_open {
137            self.write_end()?;
138            self.delete_open = false;
139        }
140        Ok(())
141    }
142
143    /// Begin a batched delete step.
144    fn write_delete_step(&mut self) -> Result<(), Error> {
145        if self.binary {
146            self.target.write_all(b"d")?;
147        } else {
148            self.target.write_all(b"d ")?;
149        }
150        Ok(())
151    }
152
153    /// Begin a clause addition step.
154    fn write_add_step(&mut self) -> Result<(), Error> {
155        if self.binary {
156            self.target.write_all(b"a")?;
157        }
158        Ok(())
159    }
160
161    /// Write a list of clause ids.
162    fn write_ids(&mut self, ids: &[u64]) -> Result<(), Error> {
163        if self.binary {
164            for &id in ids {
165                leb128::write::unsigned(&mut self.target, (id + 1) * 2)?;
166            }
167        } else {
168            for &id in ids {
169                itoa::write(&mut self.target, id + 1)?;
170                self.target.write_all(b" ")?;
171            }
172        }
173        Ok(())
174    }
175
176    /// Write a list of literals.
177    fn write_lits(&mut self, lits: &[Lit]) -> Result<(), Error> {
178        if self.binary {
179            for &lit in lits {
180                leb128::write::unsigned(&mut self.target, lit.code() as u64 + 2)?;
181            }
182        } else {
183            for &lit in lits {
184                itoa::write(&mut self.target, lit.to_dimacs())?;
185                self.target.write_all(b" ")?;
186            }
187        }
188        Ok(())
189    }
190
191    /// End the current step.
192    fn write_end(&mut self) -> Result<(), Error> {
193        if self.binary {
194            self.target.write_all(&[0])?
195        } else {
196            self.target.write_all(b"0\n")?
197        }
198        Ok(())
199    }
200
201    /// Write a separator.
202    fn write_sep(&mut self) -> Result<(), Error> {
203        if self.binary {
204            self.target.write_all(&[0])?
205        } else {
206            self.target.write_all(b"0 ")?
207        }
208        Ok(())
209    }
210}
211
212impl<'a> Drop for WriteLrat<'a> {
213    fn drop(&mut self) {
214        let _ignore_errors = self.close_delete();
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    use proptest::prelude::*;
223
224    use std::{
225        fs::File,
226        path::PathBuf,
227        process::{Command, Stdio},
228    };
229
230    use tempfile::TempDir;
231
232    use varisat::{dimacs::write_dimacs, ProofFormat, Solver};
233    use varisat_checker::Checker;
234    use varisat_formula::{cnf_formula, test::sgen_unsat_formula, CnfFormula};
235
236    fn check_lrat(tool: &str, cnf_file: &PathBuf, proof_file: &PathBuf) -> Result<bool, Error> {
237        let mut child = Command::new(tool)
238            .stdin(Stdio::piped())
239            .stdout(Stdio::piped())
240            .spawn()?;
241
242        let mut stdin = child.stdin.as_mut().unwrap();
243        writeln!(&mut stdin, "(lrat-check {:?} {:?})", cnf_file, proof_file)?;
244
245        let output = child.wait_with_output()?;
246        let stdout = std::str::from_utf8(&output.stdout)?;
247
248        Ok(stdout.contains("s VERIFIED"))
249    }
250
251    fn solve_and_check_lrat(
252        formula: CnfFormula,
253        binary: bool,
254        direct: bool,
255    ) -> Result<bool, Error> {
256        let tmp = TempDir::new()?;
257
258        let lrat_proof = tmp.path().join("proof.lrat");
259        let cnf_file = tmp.path().join("input.cnf");
260
261        let mut dimacs = vec![];
262        let mut proof = vec![];
263
264        let mut write_lrat = WriteLrat::new(File::create(&lrat_proof)?, binary);
265        write_dimacs(&mut File::create(&cnf_file)?, &formula)?;
266
267        let mut solver = Solver::new();
268
269        write_dimacs(&mut dimacs, &formula).unwrap();
270
271        if direct {
272            solver.add_proof_processor(&mut write_lrat);
273        } else {
274            solver.write_proof(&mut proof, ProofFormat::Varisat);
275        }
276
277        solver.add_dimacs_cnf(&mut &dimacs[..]).unwrap();
278
279        assert_eq!(solver.solve().ok(), Some(false));
280
281        solver.close_proof()?;
282
283        drop(solver);
284
285        if !direct {
286            let mut checker = Checker::new();
287            checker.add_processor(&mut write_lrat);
288
289            checker.add_dimacs_cnf(&mut &dimacs[..]).unwrap();
290
291            checker.check_proof(&mut &proof[..]).unwrap();
292        }
293
294        drop(write_lrat);
295
296        check_lrat(
297            if binary { "check-clrat" } else { "check-lrat" },
298            &cnf_file,
299            &lrat_proof,
300        )
301    }
302
303    #[cfg_attr(not(test_check_lrat), ignore)]
304    #[test]
305    fn duplicated_clause_lrat() {
306        for &binary in [false, true].iter() {
307            for &direct in [false, true].iter() {
308                assert!(
309                    solve_and_check_lrat(
310                        cnf_formula![
311                            1, 2;
312                            1, 2;
313                            -1, -2;
314                            3;
315                            -3, -1, 2;
316                            -4, 1, -2;
317                            4;
318                        ],
319                        binary,
320                        direct
321                    )
322                    .unwrap(),
323                    "binary: {:?} direct: {:?}",
324                    binary,
325                    direct
326                );
327            }
328        }
329    }
330
331    #[cfg_attr(not(test_check_lrat), ignore)]
332    #[test]
333    fn unit_conflict_lrat() {
334        for &binary in [false, true].iter() {
335            for &direct in [false, true].iter() {
336                assert!(
337                    solve_and_check_lrat(
338                        cnf_formula![
339                            1;
340                            2, 3;
341                            -1;
342                            4, 5;
343                        ],
344                        binary,
345                        direct
346                    )
347                    .unwrap(),
348                    "binary: {:?} direct: {:?}",
349                    binary,
350                    direct
351                );
352            }
353        }
354    }
355
356    proptest! {
357
358        #[cfg_attr(not(test_check_lrat), ignore)]
359        #[test]
360        fn sgen_unsat_lrat(
361            formula in sgen_unsat_formula(1..7usize),
362            binary in proptest::bool::ANY,
363            direct in proptest::bool::ANY,
364        ) {
365            prop_assert!(solve_and_check_lrat(formula, binary, direct).unwrap());
366        }
367    }
368}