use super::smtlib_script::SmtLibScript;
use miette::Diagnostic;
use std::ffi::OsStr;
use std::future::Future;
use std::process::Stdio;
use thiserror::Error;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
pub enum Decision {
Sat,
Unsat,
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
pub enum DecisionWithModel {
Sat {
model: String,
},
Unsat,
Unknown,
}
#[derive(Debug, Diagnostic, Error)]
pub enum SolverError {
#[error("IO error during a solver operation")]
Io(#[from] std::io::Error),
#[error("solver error: {0}")]
Solver(String),
#[error("unrecognized solver output: {0}")]
UnrecognizedSolverOutput(String),
#[error("solver was marked as failed")]
SolverMarkedFailed,
}
type Result<T> = std::result::Result<T, SolverError>;
pub trait Solver {
fn smtlib_input(&mut self) -> &mut (dyn tokio::io::AsyncWrite + Unpin + Send);
fn enable_models(&mut self) -> impl Future<Output = Result<()>> + Send;
fn check_sat(&mut self) -> impl Future<Output = Result<Decision>> + Send;
fn check_sat_with_model(&mut self) -> impl Future<Output = Result<DecisionWithModel>> + Send;
}
#[derive(Debug)]
pub struct LocalSolver {
child: Child,
solver_stdin: BufWriter<ChildStdin>,
solver_stdout: BufReader<ChildStdout>,
#[expect(unused, reason = "included for completeness")]
solver_stderr: BufReader<ChildStderr>,
}
impl LocalSolver {
pub fn from_command(cmd: &mut Command) -> Result<Self> {
let mut child = cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let (stdin, stdout, stderr) =
match (child.stdin.take(), child.stdout.take(), child.stderr.take()) {
(Some(stdin), Some(stdout), Some(stderr)) => (stdin, stdout, stderr),
_ => {
return Err(SolverError::Solver(
"Failed to fetch IO pipes for solver process".into(),
))
}
};
Ok(Self {
solver_stdin: BufWriter::new(stdin),
solver_stdout: BufReader::new(stdout),
solver_stderr: BufReader::new(stderr),
child,
})
}
pub fn cvc5() -> Result<Self> {
Self::cvc5_with_args(["--tlimit=60000"])
}
pub fn cvc5_with_args(args: impl IntoIterator<Item = impl AsRef<OsStr>>) -> Result<Self> {
let path = std::env::var("CVC5").unwrap_or_else(|_| "cvc5".into());
Self::from_command(Command::new(path).args(["--lang", "smt"]).args(args))
}
}
impl Solver for LocalSolver {
fn smtlib_input(&mut self) -> &mut (dyn tokio::io::AsyncWrite + Unpin + Send) {
&mut self.solver_stdin
}
async fn enable_models(&mut self) -> Result<()> {
self.smtlib_input()
.set_option("produce-models", "true")
.await
.map_err(Into::into)
}
async fn check_sat(&mut self) -> Result<Decision> {
self.check_child_process_status().await?;
self.smtlib_input().check_sat().await?;
self.solver_stdin.flush().await?;
let mut output = String::new();
self.read_line(&mut output).await?;
match output.as_str().trim() {
"sat" => Ok(Decision::Sat),
"unsat" => Ok(Decision::Unsat),
"unknown" => Ok(Decision::Unknown),
s => Err(self.process_error_output(s).await),
}
}
async fn check_sat_with_model(&mut self) -> Result<DecisionWithModel> {
match self.check_sat().await? {
Decision::Sat => {
self.smtlib_input().get_model().await?;
self.solver_stdin.flush().await?;
let mut output = String::new();
self.read_line(&mut output).await?;
match output.as_str().trim() {
"(" => {
loop {
let len: usize = self.read_line(&mut output).await?;
#[expect(
clippy::string_slice,
reason = "`output.len() - len` gives the end index of `output` before the `read_line`"
)]
if output[output.len() - len..].trim() == ")" {
break;
}
}
Ok(DecisionWithModel::Sat { model: output })
}
s => Err(self.process_error_output(s).await),
}
}
Decision::Unsat => Ok(DecisionWithModel::Unsat),
Decision::Unknown => Ok(DecisionWithModel::Unknown),
}
}
}
impl LocalSolver {
async fn check_child_process_status(&mut self) -> Result<()> {
if let Some(status) = self.child.try_wait()? {
Err(SolverError::Solver(format!(
"Solver process terminated unexpectedly with status: {:?}",
status.code()
)))?
}
Ok(())
}
async fn read_line(&mut self, buffer: &mut String) -> Result<usize> {
let len = self.solver_stdout.read_line(buffer).await?;
if len == 0 {
self.clean_up().await?;
self.check_child_process_status().await?;
}
Ok(len)
}
async fn process_error_output(&mut self, s: &str) -> SolverError {
match s
.strip_prefix("(error \"")
.and_then(|s| s.strip_suffix("\")"))
{
Some(e) => {
if e.starts_with("Parse Error: ") {
let _ = self.clean_up().await;
}
SolverError::Solver(e.to_string())
}
_ => SolverError::UnrecognizedSolverOutput(s.to_string()),
}
}
pub async fn clean_up(&mut self) -> Result<()> {
self.child.kill().await.map_err(|e| e.into())
}
}
#[derive(Debug)]
pub struct WriterSolver<W> {
pub w: W,
}
impl<W: tokio::io::AsyncWrite + Unpin + Send> Solver for WriterSolver<W> {
fn smtlib_input(&mut self) -> &mut (dyn tokio::io::AsyncWrite + Unpin + Send) {
&mut self.w
}
async fn enable_models(&mut self) -> Result<()> {
self.smtlib_input()
.set_option("produce-models", "true")
.await
.map_err(Into::into)
}
async fn check_sat(&mut self) -> Result<Decision> {
self.smtlib_input().check_sat().await?;
self.w.flush().await?;
Ok(Decision::Unknown)
}
async fn check_sat_with_model(&mut self) -> Result<DecisionWithModel> {
self.smtlib_input().check_sat().await?;
self.w.flush().await?;
Ok(DecisionWithModel::Unknown)
}
}
#[cfg(test)]
mod test {
use cool_asserts::assert_matches;
use super::*;
#[tokio::test]
async fn empty_cvc5_run() {
let mut my_solver = LocalSolver::cvc5().unwrap();
let decision = my_solver.check_sat().await.unwrap();
assert_eq!(decision, Decision::Sat);
}
#[tokio::test]
async fn set_logic_test() {
let mut my_solver = LocalSolver::cvc5().unwrap();
my_solver.smtlib_input().set_logic("ALL").await.unwrap();
let decision = my_solver.check_sat().await.unwrap();
assert_eq!(decision, Decision::Sat);
}
#[tokio::test]
async fn comment_test() {
let mut my_solver = LocalSolver::cvc5().unwrap();
my_solver
.smtlib_input()
.comment("(assert false)")
.await
.unwrap();
let decision = my_solver.check_sat().await.unwrap();
assert_eq!(decision, Decision::Sat);
}
#[tokio::test]
async fn comment_escaping_test() {
let mut my_solver = LocalSolver::cvc5().unwrap();
my_solver
.smtlib_input()
.comment("\n(assert false)")
.await
.unwrap();
let decision = my_solver.check_sat().await.unwrap();
assert_eq!(decision, Decision::Sat);
}
#[tokio::test]
async fn unsat_test() {
let mut my_solver = LocalSolver::cvc5().unwrap();
my_solver.smtlib_input().assert("false").await.unwrap();
let decision = my_solver.check_sat().await.unwrap();
assert_eq!(decision, Decision::Unsat);
}
#[tokio::test]
async fn get_model_sat() {
let mut my_solver = LocalSolver::cvc5().unwrap();
my_solver.enable_models().await.unwrap();
my_solver.smtlib_input().assert("true").await.unwrap();
let decision = my_solver.check_sat_with_model().await.unwrap();
assert_matches!(decision, DecisionWithModel::Sat { model } => {
assert!(!model.is_empty());
});
}
#[tokio::test]
async fn get_model_unsat() {
let mut my_solver = LocalSolver::cvc5().unwrap();
my_solver.enable_models().await.unwrap();
my_solver.smtlib_input().assert("false").await.unwrap();
let decision = my_solver.check_sat_with_model().await.unwrap();
assert_eq!(decision, DecisionWithModel::Unsat);
}
#[tokio::test]
async fn parse_error_test() {
let mut my_solver = LocalSolver::cvc5().unwrap();
my_solver.smtlib_input().assert("tomato").await.unwrap();
assert_matches!(my_solver.check_sat().await, Err(SolverError::Solver(_)));
my_solver.smtlib_input().reset().await.unwrap();
assert_matches!(my_solver.check_sat().await, Err(SolverError::Solver(x)) => assert!(x.starts_with("Solver process terminated unexpectedly with status: ")));
}
#[tokio::test]
async fn clean_up_succeeds() {
let mut my_solver = LocalSolver::cvc5().unwrap();
my_solver.clean_up().await.unwrap();
let status = my_solver.child.try_wait().unwrap();
assert!(status.is_some());
}
#[tokio::test]
async fn check_sat_crlf_test() {
let mut cmd = Command::new("sh");
cmd.args(["-c", "read line && printf 'sat\r\n'"]);
let mut solver = LocalSolver::from_command(&mut cmd).unwrap();
let decision = solver.check_sat().await.unwrap();
assert_eq!(decision, Decision::Sat);
}
#[tokio::test]
async fn check_sat_with_model_crlf_test() {
let mut cmd = Command::new("sh");
cmd.args([
"-c",
"read line && printf 'sat\\r\\n' && read line && printf '(\\r\\n define-fun x () Int 0\\r\\n)\\r\\n'",
]);
let mut solver = LocalSolver::from_command(&mut cmd).unwrap();
let decision = solver.check_sat_with_model().await.unwrap();
assert_matches!(decision, DecisionWithModel::Sat { model } => {
assert_eq!(model, "(\r\n define-fun x () Int 0\r\n)\r\n");
});
}
#[tokio::test]
async fn process_error_output_parse_error_no_line_ending_test() {
let mut solver = LocalSolver::cvc5().unwrap();
let res = solver
.process_error_output("(error \"Parse Error: mock windows error\")")
.await;
assert_matches!(res, SolverError::Solver(msg) => {
assert_eq!(msg, "Parse Error: mock windows error");
});
}
}