patronus/smt/
solver.rs

1// Copyright 2024 Cornell University
2// released under BSD 3-Clause License
3// author: Kevin Laeufer <laeufer@cornell.edu>
4
5use crate::expr::{Context, ExprRef};
6use crate::smt::parser::{SmtParserError, parse_get_value_response};
7use crate::smt::serialize::serialize_cmd;
8use std::io::{BufRead, BufReader, BufWriter};
9use std::io::{Read, Write};
10use std::process::{Command, Stdio};
11use thiserror::Error;
12
13/// A SMT Solver Error.
14#[derive(Error, Debug)]
15pub enum Error {
16    #[error("[smt] I/O operation failed")]
17    Io(#[from] std::io::Error),
18    #[error("[smt] cannot pop because the stack is already empty")]
19    StackUnderflow,
20    #[error("[smt] {0} reported an error:\n{1}")]
21    FromSolver(String, String),
22    #[error("[smt]{0} is unreachable, the process might have died")]
23    SolverDead(String),
24    #[error("[smt] {0} returned an unexpected response:\n{1}")]
25    UnexpectedResponse(String, String),
26    #[error("[smt] failed to parse a response")]
27    Parser(#[from] SmtParserError),
28}
29
30pub type Result<T> = std::result::Result<T, Error>;
31
32/// An SMT Logic.
33#[derive(Debug, Clone, Eq, PartialEq)]
34pub enum Logic {
35    All,
36    QfAufbv,
37    QfAbv,
38}
39
40impl Logic {
41    pub(crate) fn to_smt_str(&self) -> &'static str {
42        match self {
43            Logic::All => "ALL",
44            Logic::QfAufbv => "QF_AUFBV",
45            Logic::QfAbv => "QF_ABV",
46        }
47    }
48}
49
50/// A command to an SMT solver.
51#[derive(Debug, Clone, Eq, PartialEq)]
52pub enum SmtCommand {
53    Exit,
54    CheckSat,
55    SetLogic(Logic),
56    SetOption(String, String),
57    SetInfo(String, String),
58    Assert(ExprRef),
59    DeclareConst(ExprRef),
60    DefineConst(ExprRef, ExprRef),
61    CheckSatAssuming(Vec<ExprRef>),
62    Push(u64),
63    Pop(u64),
64    GetValue(ExprRef),
65}
66
67/// The result of a `(check-sat)` command.
68#[derive(Debug, Clone, Eq, PartialEq)]
69pub enum CheckSatResponse {
70    Sat,
71    Unsat,
72    Unknown,
73}
74
75/// Represents the meta data of an SMT Solver
76pub trait SolverMetaData {
77    // properties
78    fn name(&self) -> &str;
79    fn supports_check_assuming(&self) -> bool;
80    fn supports_uf(&self) -> bool;
81    /// Indicates whether the solver supports the non-standard `(as const)` command.
82    fn supports_const_array(&self) -> bool;
83}
84
85/// Allows an SMT solver to start a Context.
86pub trait Solver<R: Write + Send>: SolverMetaData {
87    type Context: SolverContext;
88    /// Launch a new instance of this solver.
89    fn start(&self, replay_file: Option<R>) -> Result<Self::Context>;
90}
91
92/// Interface to a running SMT Solver with everything executing as blocking I/O.
93pub trait SolverContext: SolverMetaData {
94    // type Replay : Write + Send;
95    fn restart(&mut self) -> Result<()>;
96    fn set_logic(&mut self, option: Logic) -> Result<()>;
97    fn assert(&mut self, ctx: &Context, e: ExprRef) -> Result<()>;
98    fn declare_const(&mut self, ctx: &Context, symbol: ExprRef) -> Result<()>;
99    fn define_const(&mut self, ctx: &Context, symbol: ExprRef, expr: ExprRef) -> Result<()>;
100    fn check_sat_assuming(
101        &mut self,
102        ctx: &Context,
103        props: impl IntoIterator<Item = ExprRef>,
104    ) -> Result<CheckSatResponse>;
105    fn check_sat(&mut self) -> Result<CheckSatResponse>;
106    fn push(&mut self) -> Result<()>;
107    fn pop(&mut self) -> Result<()>;
108    fn get_value(&mut self, ctx: &mut Context, e: ExprRef) -> Result<ExprRef>;
109}
110
111#[derive(Debug, Clone, Eq, PartialEq)]
112pub struct SmtLibSolver {
113    name: &'static str,
114    args: &'static [&'static str],
115    options: &'static [&'static str],
116    supports_uf: bool,
117    supports_check_assuming: bool,
118    supports_const_array: bool,
119}
120
121impl SolverMetaData for SmtLibSolver {
122    fn name(&self) -> &str {
123        self.name
124    }
125
126    fn supports_check_assuming(&self) -> bool {
127        self.supports_check_assuming
128    }
129
130    fn supports_uf(&self) -> bool {
131        self.supports_uf
132    }
133
134    fn supports_const_array(&self) -> bool {
135        self.supports_const_array
136    }
137}
138
139impl<R: Write + Send> Solver<R> for SmtLibSolver {
140    type Context = SmtLibSolverCtx<R>;
141    fn start(&self, replay_file: Option<R>) -> Result<Self::Context> {
142        let mut proc = Command::new(self.name)
143            .args(self.args)
144            .stdin(Stdio::piped())
145            .stdout(Stdio::piped())
146            .stderr(Stdio::piped())
147            .spawn()?;
148        let stdin = BufWriter::new(proc.stdin.take().unwrap());
149        let stdout = BufReader::new(proc.stdout.take().unwrap());
150        let stderr = proc.stderr.take().unwrap();
151        let mut solver = SmtLibSolverCtx {
152            name: self.name.to_string(),
153            proc,
154            stdin,
155            stdout,
156            stderr,
157            stack_depth: 0,
158            response: String::new(),
159            replay_file,
160            has_error: false,
161            solver_args: self.args.iter().map(|a| a.to_string()).collect(),
162            solver_options: self.options.iter().map(|a| a.to_string()).collect(),
163            supports_uf: self.supports_uf,
164            supports_check_assuming: self.supports_check_assuming,
165            supports_const_array: self.supports_const_array,
166        };
167        for option in self.options.iter() {
168            solver.write_cmd(
169                None,
170                &SmtCommand::SetOption(option.to_string(), "true".to_string()),
171            )?
172        }
173        Ok(solver)
174    }
175}
176
177/// Launches an SMT solver and communicates through `stdin` using SMTLib commands.
178pub struct SmtLibSolverCtx<R: Write + Send> {
179    name: String,
180    proc: std::process::Child,
181    stdin: BufWriter<std::process::ChildStdin>,
182    stdout: BufReader<std::process::ChildStdout>,
183    stderr: std::process::ChildStderr,
184    stack_depth: usize,
185    response: String,
186    replay_file: Option<R>,
187    /// keeps track of whether there was an error from the solver which might make regular shutdown
188    /// impossible
189    has_error: bool,
190    // metadata to be able to restart solver
191    solver_args: Vec<String>,
192    solver_options: Vec<String>,
193    // solver capabilities
194    supports_uf: bool,
195    supports_check_assuming: bool,
196    supports_const_array: bool,
197}
198
199impl<R: Write + Send> SmtLibSolverCtx<R> {
200    #[inline]
201    fn write_cmd(&mut self, ctx: Option<&Context>, cmd: &SmtCommand) -> Result<()> {
202        if let Some(rf) = self.replay_file.as_mut() {
203            serialize_cmd(rf, ctx, cmd)?;
204        }
205        serialize_cmd(&mut self.stdin, ctx, cmd)?;
206        if let Some(rf) = self.replay_file.as_mut() {
207            rf.flush()?;
208        }
209        match self.stdin.flush() {
210            Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
211                // make sure we drop the replay file
212                let _ = self.replay_file.take();
213                // check to see if we can find an error message
214                match self.read_response() {
215                    Err(e @ Error::FromSolver(_, _)) => Err(e),
216                    _ => Err(Error::SolverDead(self.name.clone())),
217                }
218            }
219            Err(other) => Err(other.into()),
220            Ok(_) => Ok(()),
221        }
222    }
223
224    /// after this function executes, the result will be available in `self.result`.
225    fn read_response(&mut self) -> Result<()> {
226        self.response.clear();
227        // our basic assumptions are:
228        // 1. the solver will terminate its answer with '\n'
229        // 2. the answer will contain a balanced number of parenthesis
230        self.stdout.read_line(&mut self.response)?;
231        while count_parens(&self.response) > 0 {
232            self.response.push(' ');
233            self.stdout.read_line(&mut self.response)?;
234        }
235
236        // check to see if there was an error reported on stdout
237        if self.response.trim_start().starts_with("(error") {
238            let trimmed = self.response.trim();
239            let start = "(error ".len();
240            let msg = &trimmed[start..(trimmed.len() - start - 1)];
241            self.has_error = true;
242            Err(Error::FromSolver(self.name.clone(), msg.to_string()))
243        } else {
244            // check if the process is still alive
245            match self.proc.try_wait() {
246                Ok(Some(status)) if !status.success() => {
247                    // solver terminated with error return code
248                    // check for output on stderror
249                    let mut err = vec![];
250                    self.stderr.read_to_end(&mut err)?;
251                    self.has_error = true;
252                    Err(Error::FromSolver(
253                        self.name.clone(),
254                        String::from_utf8_lossy(&err).to_string(),
255                    ))
256                }
257                _ => Ok(()),
258            }
259        }
260    }
261
262    fn read_sat_response(&mut self) -> Result<CheckSatResponse> {
263        self.stdin.flush()?; // make sure that the commands reached the solver
264        self.read_response()?;
265        let response = self.response.trim();
266        match response {
267            "sat" => Ok(CheckSatResponse::Sat),
268            "unsat" => Ok(CheckSatResponse::Unsat),
269            other => Err(Error::UnexpectedResponse(
270                self.name.clone(),
271                other.to_string(),
272            )),
273        }
274    }
275}
276
277fn count_parens(s: &str) -> i64 {
278    s.chars().fold(0, |count, cc| match cc {
279        '(' => count + 1,
280        ')' => count - 1,
281        _ => count,
282    })
283}
284
285impl<R: Write + Send> Drop for SmtLibSolverCtx<R> {
286    fn drop(&mut self) {
287        shut_down_solver(self);
288    }
289}
290
291/// internal method to try to cleanly shut down the solver process
292fn shut_down_solver<R: Write + Send>(solver: &mut SmtLibSolverCtx<R>) {
293    // try to close the child process as not to leak resources
294    if solver.write_cmd(None, &SmtCommand::Exit).is_ok() {
295        let _status = solver
296            .proc
297            .wait()
298            .expect("failed to wait for SMT solver to exit");
299    }
300    // we don't care whether the solver crashed or returned success, as long as it is cleaned up
301}
302
303impl<R: Write + Send> SolverMetaData for SmtLibSolverCtx<R> {
304    fn name(&self) -> &str {
305        &self.name
306    }
307
308    fn supports_uf(&self) -> bool {
309        self.supports_uf
310    }
311    fn supports_check_assuming(&self) -> bool {
312        self.supports_check_assuming
313    }
314
315    fn supports_const_array(&self) -> bool {
316        self.supports_const_array
317    }
318}
319
320impl<R: Write + Send> SolverContext for SmtLibSolverCtx<R> {
321    fn restart(&mut self) -> Result<()> {
322        shut_down_solver(self);
323
324        let mut proc = Command::new(&self.name)
325            .args(&self.solver_args)
326            .stdin(Stdio::piped())
327            .stdout(Stdio::piped())
328            .stderr(Stdio::piped())
329            .spawn()?;
330        let stdin = BufWriter::new(proc.stdin.take().unwrap());
331        let stdout = BufReader::new(proc.stdout.take().unwrap());
332        let stderr = proc.stderr.take().unwrap();
333        self.proc = proc;
334        self.stdin = stdin;
335        self.stdout = stdout;
336        self.stderr = stderr;
337        for option in self.solver_options.clone() {
338            self.write_cmd(None, &SmtCommand::SetOption(option, "true".to_string()))?;
339        }
340        Ok(())
341    }
342
343    fn set_logic(&mut self, logic: Logic) -> Result<()> {
344        self.write_cmd(None, &SmtCommand::SetLogic(logic))
345    }
346
347    fn assert(&mut self, ctx: &Context, e: ExprRef) -> Result<()> {
348        self.write_cmd(Some(ctx), &SmtCommand::Assert(e))
349    }
350
351    fn declare_const(&mut self, ctx: &Context, symbol: ExprRef) -> Result<()> {
352        self.write_cmd(Some(ctx), &SmtCommand::DeclareConst(symbol))
353    }
354
355    fn define_const(&mut self, ctx: &Context, symbol: ExprRef, expr: ExprRef) -> Result<()> {
356        self.write_cmd(Some(ctx), &SmtCommand::DefineConst(symbol, expr))
357    }
358
359    fn check_sat_assuming(
360        &mut self,
361        ctx: &Context,
362        props: impl IntoIterator<Item = ExprRef>,
363    ) -> Result<CheckSatResponse> {
364        let props: Vec<ExprRef> = props.into_iter().collect();
365        self.write_cmd(Some(ctx), &SmtCommand::CheckSatAssuming(props))?;
366        self.read_sat_response()
367    }
368
369    fn check_sat(&mut self) -> Result<CheckSatResponse> {
370        self.write_cmd(None, &SmtCommand::CheckSat)?;
371        self.read_sat_response()
372    }
373
374    fn push(&mut self) -> Result<()> {
375        self.write_cmd(None, &SmtCommand::Push(1))?;
376        self.stack_depth += 1;
377        Ok(())
378    }
379
380    fn pop(&mut self) -> Result<()> {
381        if self.stack_depth > 0 {
382            self.write_cmd(None, &SmtCommand::Pop(1))?;
383            self.stack_depth -= 1;
384            Ok(())
385        } else {
386            Err(Error::StackUnderflow)
387        }
388    }
389
390    fn get_value(&mut self, ctx: &mut Context, e: ExprRef) -> Result<ExprRef> {
391        self.write_cmd(Some(ctx), &SmtCommand::GetValue(e))?;
392        self.stdin.flush()?; // make sure that the commands reached the solver
393        self.read_response()?;
394        let response = self.response.trim();
395        let expr = parse_get_value_response(ctx, response.as_bytes())?;
396        Ok(expr)
397    }
398}
399
400pub const BITWUZLA: SmtLibSolver = SmtLibSolver {
401    name: "bitwuzla",
402    args: &[],
403    options: &["incremental", "produce-models"],
404    supports_uf: false,
405    supports_check_assuming: true,
406    supports_const_array: true,
407};
408
409pub const YICES2: SmtLibSolver = SmtLibSolver {
410    name: "yices-smt2",
411    args: &["--incremental"],
412    options: &[],
413    supports_uf: false, // actually true, but ignoring for now
414    supports_check_assuming: false,
415    // see https://github.com/SRI-CSL/yices2/issues/110
416    supports_const_array: false,
417};
418
419pub const Z3: SmtLibSolver = SmtLibSolver {
420    name: "z3",
421    args: &["-in"],
422    options: &[],
423    supports_uf: true,
424    supports_check_assuming: true,
425    supports_const_array: true,
426};
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    #[test]
433    fn test_bitwuzla_error() {
434        let mut ctx = Context::default();
435        let replay = Some(std::fs::File::create("replay.smt").unwrap());
436        let mut solver = BITWUZLA.start(replay).unwrap();
437        let a = ctx.bv_symbol("a", 3);
438        let e = ctx.build(|c| c.equal(a, c.bit_vec_val(3, 3)));
439        solver.assert(&ctx, e).unwrap();
440        let res = solver.check_sat();
441        assert!(res.is_err(), "a was not declared!");
442        // after this error, the solver is dead and won't respond!
443        let _res = solver.declare_const(&ctx, a);
444        // assert!(res.is_err());
445        // TODO: this does not always work? do we need to wait longer?
446    }
447
448    #[test]
449    fn test_bitwuzla() {
450        let mut ctx = Context::default();
451        let a = ctx.bv_symbol("a", 3);
452        let e = ctx.build(|c| c.equal(a, c.bit_vec_val(3, 3)));
453        let replay = Some(std::fs::File::create("replay.smt").unwrap());
454        let mut solver = BITWUZLA.start(replay).unwrap();
455        solver.declare_const(&ctx, a).unwrap();
456        let res = solver.check_sat_assuming(&ctx, [e]);
457        assert_eq!(res.unwrap(), CheckSatResponse::Sat);
458        let value_of_a = solver.get_value(&mut ctx, a).unwrap();
459        assert_eq!(value_of_a, ctx.bit_vec_val(3, 3));
460    }
461
462    #[test]
463    fn test_bitwuzla_restart() {
464        let mut ctx = Context::default();
465        let a = ctx.bv_symbol("a", 3);
466        let replay = Some(std::fs::File::create("replay.smt").unwrap());
467        let mut solver = BITWUZLA.start(replay).unwrap();
468        let three = ctx.bit_vec_val(3, 3);
469        let four = ctx.bit_vec_val(3, 3);
470        solver.define_const(&ctx, a, three).unwrap();
471        let _res = solver.check_sat().unwrap();
472        let value_of_a = solver.get_value(&mut ctx, a).unwrap();
473        assert_eq!(value_of_a, three);
474
475        // restarting bitwuzla allows us to redefine `a`
476        solver.restart().unwrap();
477        solver.define_const(&ctx, a, four).unwrap();
478        let _res = solver.check_sat().unwrap();
479        let value_of_a = solver.get_value(&mut ctx, a).unwrap();
480        assert_eq!(value_of_a, four);
481    }
482}