use std::path::PathBuf;
use std::process::Stdio;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use tokio::time::{timeout, Duration};
use anyhow::Result;
const Z3_VERSION: &str = "4.16.0";
pub async fn find_or_download_z3() -> anyhow::Result<PathBuf> {
if let Ok(p) = std::env::var("Z3_BIN") {
let pb = PathBuf::from(&p);
if pb.exists() { return Ok(pb); }
}
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
for name in ["z3", "z3.exe"] {
let candidate = dir.join(name);
if candidate.exists() { return Ok(candidate); }
}
}
}
let cache_dir = dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("z39");
let cached = cache_dir.join("z3");
if cfg!(target_os = "windows") {
let cached_exe = cache_dir.join("z3.exe");
if cached_exe.exists() { return Ok(cached_exe); }
}
if cached.exists() { return Ok(cached); }
for dir in std::env::var("PATH").unwrap_or_default().split(':') {
let p = PathBuf::from(dir).join("z3");
if p.exists() { return Ok(p); }
}
eprintln!("z39: z3 not found, auto-provisioning v{}...", Z3_VERSION);
provision_z3(&cache_dir).await?;
if cfg!(target_os = "windows") {
Ok(cache_dir.join("z3.exe"))
} else {
Ok(cache_dir.join("z3"))
}
}
async fn provision_z3(cache_dir: &PathBuf) -> anyhow::Result<()> {
std::fs::create_dir_all(cache_dir)?;
#[cfg(target_os = "macos")]
{
let arch = if cfg!(target_arch = "aarch64") { "arm64" } else { "x64" };
let filename = format!("z3-{Z3_VERSION}-{arch}-osx-15.7.3.zip");
let url = format!("https://github.com/Z3Prover/z3/releases/download/z3-{Z3_VERSION}/{filename}");
download_and_extract(&url, cache_dir).await
}
#[cfg(target_os = "windows")]
{
let filename = format!("z3-{Z3_VERSION}-x64-win.zip");
let url = format!("https://github.com/Z3Prover/z3/releases/download/z3-{Z3_VERSION}/{filename}");
download_and_extract(&url, cache_dir).await
}
#[cfg(target_os = "linux")]
{
build_z3_from_source(cache_dir).await
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
{
anyhow::bail!("z39: unsupported platform for auto-provisioning; install z3 manually and set Z3_BIN")
}
}
async fn download_and_extract(url: &str, cache_dir: &PathBuf) -> anyhow::Result<()> {
eprintln!("z39: downloading {url}");
let response = reqwest::get(url).await?;
if !response.status().is_success() {
anyhow::bail!("download failed: {}", response.status());
}
let bytes = response.bytes().await?;
let tmpdir = tempfile::tempdir()?;
let zip_path = tmpdir.path().join("z3.zip");
std::fs::write(&zip_path, &bytes)?;
let archive = std::io::BufReader::new(std::fs::File::open(&zip_path)?);
let mut zip = zip::ZipArchive::new(archive)?;
let dest_name = if cfg!(target_os = "windows") { "z3.exe" } else { "z3" };
let dest = cache_dir.join(dest_name);
for i in 0..zip.len() {
let mut file = zip.by_index(i)?;
let name = file.name().to_string();
if name.ends_with("/bin/z3") || name.ends_with("/bin/z3.exe") || name == "z3" || name == "z3.exe" {
let mut buf = Vec::new();
std::io::Read::read_to_end(&mut file, &mut buf)?;
std::fs::write(&dest, &buf)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(0o755))?;
}
eprintln!("z39: z3 installed to {}", dest.display());
return Ok(());
}
}
anyhow::bail!("z3 binary not found in archive")
}
#[cfg(target_os = "linux")]
async fn build_z3_from_source(cache_dir: &PathBuf) -> anyhow::Result<()> {
let src_url = format!("https://github.com/Z3Prover/z3/archive/refs/tags/z3-{Z3_VERSION}.tar.gz");
let tmpdir = tempfile::tempdir()?;
eprintln!("z39: downloading Z3 source...");
let response = reqwest::get(&src_url).await?;
if !response.status().is_success() {
anyhow::bail!("download failed: {}", response.status());
}
let bytes = response.bytes().await?;
let archive_path = tmpdir.path().join("z3.tar.gz");
std::fs::write(&archive_path, &bytes)?;
let output = Command::new("tar")
.args(["-xzf", archive_path.to_str().unwrap(), "-C", tmpdir.path().to_str().unwrap()])
.output().await?;
if !output.status.success() {
anyhow::bail!("failed to extract Z3 source: {}", String::from_utf8_lossy(&output.stderr));
}
let src_dir = tmpdir.path().join(format!("z3-z3-{Z3_VERSION}"));
let build_dir = src_dir.join("build");
std::fs::create_dir_all(&build_dir)?;
eprintln!("z39: configuring Z3...");
let output = Command::new("python3")
.args(["../scripts/mk_make.py"])
.current_dir(&build_dir)
.output().await?;
if !output.status.success() {
anyhow::bail!("Z3 configure failed: {}", String::from_utf8_lossy(&output.stderr));
}
let nproc = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
eprintln!("z39: building Z3 with {nproc} jobs (this may take a few minutes)...");
let output = Command::new("make")
.args(["-j", &nproc.to_string()])
.current_dir(&build_dir)
.output().await?;
if !output.status.success() {
anyhow::bail!("Z3 build failed: {}", String::from_utf8_lossy(&output.stderr));
}
let built = build_dir.join("z3");
let dest = cache_dir.join("z3");
std::fs::copy(&built, &dest)?;
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(0o755))?;
eprintln!("z39: Z3 v{} built and installed to {}", Z3_VERSION, dest.display());
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
pub enum SolveStatus {
Sat,
Unsat,
Unknown,
Timeout,
Error(String),
}
#[derive(Debug, Clone)]
pub struct SolveResult {
pub status: SolveStatus,
pub model: Option<String>,
pub raw_output: String,
pub duration_ms: u64,
}
impl SolveResult {
pub fn to_compact(&self) -> String {
let t = format!("{:.1}s", self.duration_ms as f64 / 1000.0);
match &self.status {
SolveStatus::Sat => {
let m = self.model.as_deref().unwrap_or("");
if m.is_empty() { format!("sat {t}") } else { format!("sat {m} {t}") }
}
SolveStatus::Unsat => format!("unsat {t}"),
SolveStatus::Unknown => format!("unknown {t}"),
SolveStatus::Timeout => format!("timeout {t}"),
SolveStatus::Error(e) => format!("error {e} {t}"),
}
}
pub fn is_sat(&self) -> bool { self.status == SolveStatus::Sat }
pub fn is_unsat(&self) -> bool { self.status == SolveStatus::Unsat }
}
pub async fn solve(z3_bin: &PathBuf, smt_input: &str, timeout_secs: u64) -> SolveResult {
let start = std::time::Instant::now();
let dur = if timeout_secs > 0 {
Duration::from_secs(timeout_secs)
} else {
Duration::from_secs(300)
};
let result = timeout(dur, run_z3_process(z3_bin, smt_input)).await;
let elapsed = start.elapsed().as_millis() as u64;
match result {
Ok(Ok(output)) => parse_z3_output(&output, elapsed),
Ok(Err(e)) => SolveResult {
status: SolveStatus::Error(e.to_string()),
model: None,
raw_output: String::new(),
duration_ms: elapsed,
},
Err(_) => SolveResult {
status: SolveStatus::Timeout,
model: None,
raw_output: String::new(),
duration_ms: elapsed,
},
}
}
async fn run_z3_process(z3_bin: &PathBuf, smt_input: &str) -> Result<String> {
let mut child = Command::new(z3_bin)
.arg("-in")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(smt_input.as_bytes()).await?;
stdin.shutdown().await?;
}
let output = child.wait_with_output().await?;
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
fn parse_z3_output(raw: &str, elapsed_ms: u64) -> SolveResult {
let trimmed = raw.trim();
let first_line = trimmed.lines().next().unwrap_or("").trim();
let (status, model) = if first_line.starts_with("sat") || first_line == "sat" {
(SolveStatus::Sat, extract_model(trimmed))
} else if first_line.starts_with("unsat") {
(SolveStatus::Unsat, None)
} else if first_line.starts_with("unknown") {
(SolveStatus::Unknown, None)
} else if first_line.starts_with("error") {
(SolveStatus::Error(first_line.to_string()), None)
} else {
(SolveStatus::Error(format!("unexpected: {first_line}")), None)
};
SolveResult { status, model, raw_output: trimmed.to_string(), duration_ms: elapsed_ms }
}
fn extract_model(output: &str) -> Option<String> {
let mut assignments = Vec::new();
let lines: Vec<&str> = output.lines().collect();
for i in 0..lines.len() {
let line = lines[i].trim();
if line.starts_with("(define-fun") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
let name = parts[1];
if parts.len() >= 5 {
let val = parts[parts.len() - 1].trim_end_matches(')');
if !val.is_empty() && val != "(" && val != "()" {
assignments.push(format!("{name}={val}"));
continue;
}
}
if i + 1 < lines.len() {
let next = lines[i + 1].trim();
let val = next.trim_end_matches(')');
if !val.is_empty() && val != "(" {
assignments.push(format!("{name}={val}"));
}
}
}
}
}
if assignments.is_empty() { None } else { Some(assignments.join(" ")) }
}