1use crate::term::{BV, Bool};
24use ordeal::{BoolTerm, CheckResult};
25
26const DEFAULT_MAX_CONFLICTS: u64 = 1_000_000;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum CheckOutcome {
35 Unsat,
38 Sat,
41 Unknown(String),
44}
45
46pub trait BvSolver {
50 fn name(&self) -> &'static str;
52
53 fn assert(&mut self, cond: &Bool);
55
56 fn check(&mut self) -> CheckOutcome;
58
59 fn value(&self, var: &BV) -> Option<u128>;
64}
65
66pub fn new_solver() -> Box<dyn BvSolver> {
72 #[cfg(feature = "z3-solver")]
73 if std::env::var("SYNTH_SOLVER_DIFF").as_deref() == Ok("1") {
74 return Box::new(z3_backend::DifferentialSolver::new());
75 }
76 Box::new(OrdealSolver::new())
77}
78
79pub struct OrdealSolver {
85 assertions: Vec<BoolTerm>,
86 max_conflicts: u64,
87 model: Option<ordeal::Model>,
88}
89
90impl OrdealSolver {
91 pub fn new() -> Self {
94 let max_conflicts = std::env::var("SYNTH_ORDEAL_MAX_CONFLICTS")
95 .ok()
96 .and_then(|v| v.parse().ok())
97 .unwrap_or(DEFAULT_MAX_CONFLICTS);
98 Self {
99 assertions: Vec::new(),
100 max_conflicts,
101 model: None,
102 }
103 }
104}
105
106impl Default for OrdealSolver {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl BvSolver for OrdealSolver {
113 fn name(&self) -> &'static str {
114 "ordeal"
115 }
116
117 fn assert(&mut self, cond: &Bool) {
118 self.assertions.push(cond.term().clone());
119 }
120
121 fn check(&mut self) -> CheckOutcome {
122 let mut solver = ordeal::Solver::new();
123 for a in &self.assertions {
124 solver.assert(a.clone());
125 }
126 let result = if self.max_conflicts == 0 {
127 solver.check()
128 } else {
129 solver.check_with_limit(self.max_conflicts)
130 };
131 match result {
132 CheckResult::Unsat(_certificate) => CheckOutcome::Unsat,
133 CheckResult::Sat(model) => {
134 self.model = Some(model);
135 CheckOutcome::Sat
136 }
137 CheckResult::Unknown => CheckOutcome::Unknown(format!(
138 "ordeal returned unknown (conflict budget {})",
139 self.max_conflicts
140 )),
141 }
142 }
143
144 fn value(&self, var: &BV) -> Option<u128> {
145 let model = self.model.as_ref()?;
146 let name = var.var_name()?;
147 Some(
148 model
149 .assignments
150 .iter()
151 .find(|(n, _)| n == name)
152 .map(|(_, v)| *v)
153 .unwrap_or(0),
156 )
157 }
158}
159
160#[cfg(feature = "z3-solver")]
165mod z3_backend {
166 use super::*;
167 use ordeal::BvTerm;
168 use z3::SatResult;
169 use z3::ast::Ast;
170
171 fn bv_to_z3(t: &BvTerm) -> z3::ast::BV {
174 match t {
175 BvTerm::Const { value, sort } => {
176 if sort.width <= 64 {
177 z3::ast::BV::from_u64(*value as u64, sort.width)
178 } else {
179 let hi = z3::ast::BV::from_u64((value >> 64) as u64, sort.width - 64);
181 let lo = z3::ast::BV::from_u64(*value as u64, 64);
182 hi.concat(&lo)
183 }
184 }
185 BvTerm::Var { name, sort } => z3::ast::BV::new_const(name.clone(), sort.width),
186 BvTerm::Add(a, b) => bv_to_z3(a).bvadd(&bv_to_z3(b)),
187 BvTerm::Sub(a, b) => bv_to_z3(a).bvsub(&bv_to_z3(b)),
188 BvTerm::Mul(a, b) => bv_to_z3(a).bvmul(&bv_to_z3(b)),
189 BvTerm::Udiv(a, b) => bv_to_z3(a).bvudiv(&bv_to_z3(b)),
190 BvTerm::And(a, b) => bv_to_z3(a).bvand(&bv_to_z3(b)),
191 BvTerm::Or(a, b) => bv_to_z3(a).bvor(&bv_to_z3(b)),
192 BvTerm::Xor(a, b) => bv_to_z3(a).bvxor(&bv_to_z3(b)),
193 BvTerm::Shl(a, b) => bv_to_z3(a).bvshl(&bv_to_z3(b)),
194 BvTerm::Lshr(a, b) => bv_to_z3(a).bvlshr(&bv_to_z3(b)),
195 BvTerm::Ashr(a, b) => bv_to_z3(a).bvashr(&bv_to_z3(b)),
196 BvTerm::Rotr(a, b) => bv_to_z3(a).bvrotr(&bv_to_z3(b)),
197 BvTerm::Extract { hi, lo, arg } => bv_to_z3(arg).extract(*hi, *lo),
198 BvTerm::Concat(a, b) => bv_to_z3(a).concat(&bv_to_z3(b)),
199 BvTerm::ZeroExt { by, arg } => bv_to_z3(arg).zero_ext(*by),
200 BvTerm::SignExt { by, arg } => bv_to_z3(arg).sign_ext(*by),
201 BvTerm::Ite { cond, then_, else_ } => {
202 bool_to_z3(cond).ite(&bv_to_z3(then_), &bv_to_z3(else_))
203 }
204 }
205 }
206
207 fn bool_to_z3(t: &BoolTerm) -> z3::ast::Bool {
208 match t {
209 BoolTerm::Eq(a, b) => bv_to_z3(a).eq(&bv_to_z3(b)),
210 BoolTerm::Ne(a, b) => bv_to_z3(a).eq(&bv_to_z3(b)).not(),
211 BoolTerm::Ult(a, b) => bv_to_z3(a).bvult(&bv_to_z3(b)),
212 BoolTerm::Ule(a, b) => bv_to_z3(a).bvule(&bv_to_z3(b)),
213 BoolTerm::Ugt(a, b) => bv_to_z3(a).bvugt(&bv_to_z3(b)),
214 BoolTerm::Uge(a, b) => bv_to_z3(a).bvuge(&bv_to_z3(b)),
215 BoolTerm::Slt(a, b) => bv_to_z3(a).bvslt(&bv_to_z3(b)),
216 BoolTerm::Sle(a, b) => bv_to_z3(a).bvsle(&bv_to_z3(b)),
217 BoolTerm::Sgt(a, b) => bv_to_z3(a).bvsgt(&bv_to_z3(b)),
218 BoolTerm::Sge(a, b) => bv_to_z3(a).bvsge(&bv_to_z3(b)),
219 BoolTerm::Not(a) => bool_to_z3(a).not(),
220 BoolTerm::And(a, b) => z3::ast::Bool::and(&[&bool_to_z3(a), &bool_to_z3(b)]),
221 BoolTerm::Or(a, b) => z3::ast::Bool::or(&[&bool_to_z3(a), &bool_to_z3(b)]),
222 }
223 }
224
225 pub struct Z3Solver {
227 assertions: Vec<BoolTerm>,
228 model: Option<z3::Model>,
229 }
230
231 impl Z3Solver {
232 pub fn new() -> Self {
233 Self {
234 assertions: Vec::new(),
235 model: None,
236 }
237 }
238 }
239
240 impl BvSolver for Z3Solver {
241 fn name(&self) -> &'static str {
242 "z3"
243 }
244
245 fn assert(&mut self, cond: &Bool) {
246 self.assertions.push(cond.term().clone());
247 }
248
249 fn check(&mut self) -> CheckOutcome {
250 let solver = z3::Solver::new();
251 for a in &self.assertions {
252 solver.assert(bool_to_z3(a));
253 }
254 match solver.check() {
255 SatResult::Unsat => CheckOutcome::Unsat,
256 SatResult::Sat => {
257 self.model = solver.get_model();
258 if self.model.is_some() {
259 CheckOutcome::Sat
260 } else {
261 CheckOutcome::Unknown("z3: SAT but no model available".to_string())
262 }
263 }
264 SatResult::Unknown => CheckOutcome::Unknown("z3 returned unknown".to_string()),
265 }
266 }
267
268 fn value(&self, var: &BV) -> Option<u128> {
269 let model = self.model.as_ref()?;
270 let z3_var = bv_to_z3(var.term());
271 model
274 .eval(&z3_var, true)
275 .and_then(|v| v.as_u64())
276 .map(u128::from)
277 }
278 }
279
280 #[derive(Clone, Copy, PartialEq)]
282 enum ModelSource {
283 Ordeal,
284 Z3,
285 }
286
287 pub struct DifferentialSolver {
293 ordeal: OrdealSolver,
294 z3: Z3Solver,
295 model_source: ModelSource,
296 }
297
298 impl DifferentialSolver {
299 pub fn new() -> Self {
300 Self {
301 ordeal: OrdealSolver::new(),
302 z3: Z3Solver::new(),
303 model_source: ModelSource::Ordeal,
304 }
305 }
306 }
307
308 impl BvSolver for DifferentialSolver {
309 fn name(&self) -> &'static str {
310 "ordeal+z3-differential"
311 }
312
313 fn assert(&mut self, cond: &Bool) {
314 self.ordeal.assert(cond);
315 self.z3.assert(cond);
316 }
317
318 fn check(&mut self) -> CheckOutcome {
319 let ordeal_verdict = self.ordeal.check();
320 let z3_verdict = self.z3.check();
321 match (&ordeal_verdict, &z3_verdict) {
322 (CheckOutcome::Unknown(reason), _) => {
326 eprintln!(
327 "[synth-verify differential] ordeal unknown ({reason}); \
328 falling through to z3 verdict: {z3_verdict:?}"
329 );
330 self.model_source = ModelSource::Z3;
331 z3_verdict
332 }
333 (_, CheckOutcome::Unknown(reason)) => {
336 eprintln!(
337 "[synth-verify differential] z3 unknown ({reason}); \
338 keeping ordeal verdict: {ordeal_verdict:?}"
339 );
340 self.model_source = ModelSource::Ordeal;
341 ordeal_verdict
342 }
343 (CheckOutcome::Unsat, CheckOutcome::Unsat) => CheckOutcome::Unsat,
344 (CheckOutcome::Sat, CheckOutcome::Sat) => {
345 self.model_source = ModelSource::Ordeal;
346 CheckOutcome::Sat
347 }
348 (o, z) => panic!(
350 "SOLVER DISAGREEMENT (#553 differential oracle): \
351 ordeal={o:?} z3={z:?} on the same query — one engine is \
352 unsound on this fragment; refusing to proceed"
353 ),
354 }
355 }
356
357 fn value(&self, var: &BV) -> Option<u128> {
358 match self.model_source {
359 ModelSource::Ordeal => self.ordeal.value(var),
360 ModelSource::Z3 => self.z3.value(var),
361 }
362 }
363 }
364}
365
366#[cfg(feature = "z3-solver")]
367pub use z3_backend::{DifferentialSolver, Z3Solver};
368
369#[cfg(test)]
374mod tests {
375 use super::*;
376
377 fn x32() -> BV {
378 BV::new_const("x", 32)
379 }
380 fn y32() -> BV {
381 BV::new_const("y", 32)
382 }
383
384 fn corpus() -> Vec<(&'static str, Bool, bool)> {
387 let x = x32();
388 let y = y32();
389 let one = BV::from_i64(1, 32);
390 vec![
391 ("add-comm", x.bvadd(&y).eq(y.bvadd(&x)).not(), true),
392 ("mul-comm", x.bvmul(&y).eq(y.bvmul(&x)).not(), true),
393 ("and-comm", x.bvand(&y).eq(y.bvand(&x)).not(), true),
394 (
395 "shl1-vs-mul2",
396 x.bvshl(&one).eq(x.bvmul(BV::from_i64(2, 32))).not(),
397 true,
398 ),
399 (
400 "sub-not-comm",
401 x.bvsub(&y).eq(y.bvsub(&x)).not(),
402 false, ),
404 (
405 "ite-select",
406 {
407 let c = x.eq(BV::from_i64(0, 32)).not();
408 c.ite(&y, &one).eq(c.ite(&y, &one)).not()
409 },
410 true,
411 ),
412 (
413 "add-vs-sub-bug",
414 x.bvadd(&y).eq(x.bvsub(&y)).not(),
415 false, ),
417 ]
418 }
419
420 #[test]
421 fn ordeal_decides_the_corpus() {
422 for (label, query, expect_unsat) in corpus() {
423 let mut solver = OrdealSolver::new();
424 solver.assert(&query);
425 let outcome = solver.check();
426 if expect_unsat {
427 assert_eq!(outcome, CheckOutcome::Unsat, "{label}");
428 } else {
429 assert_eq!(outcome, CheckOutcome::Sat, "{label}");
430 assert!(solver.value(&x32()).is_some(), "{label}: no model value");
432 }
433 }
434 }
435
436 #[test]
437 fn ordeal_finds_counterexample_with_model() {
438 let x = x32();
443 let mut solver = OrdealSolver::new();
444 solver.assert(
445 &x.bvadd(BV::from_i64(1, 32))
446 .eq(x.bvsub(BV::from_i64(1, 32))),
447 );
448 assert_eq!(solver.check(), CheckOutcome::Unsat);
449
450 let mut solver = OrdealSolver::new();
451 solver.assert(&x.eq(BV::from_i64(7, 32)));
452 assert_eq!(solver.check(), CheckOutcome::Sat);
453 assert_eq!(solver.value(&x), Some(7));
454 }
455
456 #[test]
457 fn bool_var_bridge_is_symbolic() {
458 let flag = Bool::new_const("flag");
460 let one = BV::from_i64(1, 32);
461 let zero = BV::from_i64(0, 32);
462 for (cond, expected) in [(flag.clone(), 1u128), (flag.not(), 0u128)] {
465 let mut solver = OrdealSolver::new();
466 let out = flag.ite(&one, &zero);
467 let probe = BV::new_const("probe", 32);
468 solver.assert(&probe.eq(&out));
469 solver.assert(&cond);
470 assert_eq!(solver.check(), CheckOutcome::Sat);
471 assert_eq!(solver.value(&probe), Some(expected));
472 }
473 }
474
475 #[cfg(feature = "z3-solver")]
481 #[test]
482 fn differential_zero_disagreements_on_corpus() {
483 crate::with_z3_context(|| {
484 for (label, query, expect_unsat) in corpus() {
485 let mut solver = DifferentialSolver::new();
486 solver.assert(&query);
487 let outcome = solver.check();
488 if expect_unsat {
489 assert_eq!(outcome, CheckOutcome::Unsat, "{label}");
490 } else {
491 assert_eq!(outcome, CheckOutcome::Sat, "{label}");
492 }
493 }
494 });
495 }
496}