Skip to main content

synth_verify/
term.rs

1//! Solver-agnostic bitvector / boolean term types (#553).
2//!
3//! `BV` and `Bool` wrap [`ordeal::BvTerm`] / [`ordeal::BoolTerm`] behind the
4//! exact method surface the semantics encoders (`wasm_semantics.rs`,
5//! `arm_semantics.rs`) previously used from `z3::ast::{BV, Bool}` — so the
6//! encoders stay in their native idiom while becoming solver-independent.
7//! Both backends (the default pure-Rust `ordeal` engine and the optional
8//! feature-gated Z3 differential oracle) consume these terms; see
9//! `solver.rs`.
10//!
11//! # Commutative-operand canonicalization (interim shim)
12//!
13//! ordeal 0.4 has no term normalization yet (scoped upstream for ordeal
14//! v0.8.0, pulseengine/ordeal#29): syntactically commuted operands of a
15//! commutative op (`a*b` vs `b*a`) can blast to structurally distinct AIGs
16//! and, for `Mul`, hit a CDCL cliff. Until v0.8.0 lands, the constructors for
17//! the commutative ops (`bvadd`, `bvmul`, `bvand`, `bvor`, `bvxor`, and the
18//! `Eq`/`Ne` predicates) order their operands by a deterministic structural
19//! key, so both sides of an equivalence query build the *same* term for the
20//! same commuted expression. This is a pure reordering of arguments to
21//! commutative SMT-LIB operations — semantics are untouched (the Z3 oracle
22//! sees the identical canonicalized query).
23
24use ordeal::{BoolTerm, BvTerm, Sort};
25use std::borrow::Borrow;
26use std::cmp::Ordering;
27use std::fmt;
28
29/// Bit-mask of `width` low bits (saturating at 128).
30fn mask(width: u32) -> u128 {
31    if width >= 128 {
32        u128::MAX
33    } else {
34        (1u128 << width) - 1
35    }
36}
37
38/// A bitvector term of a known width.
39///
40/// Mirrors the `z3::ast::BV` call surface used by the semantics encoders.
41#[derive(Clone, Debug)]
42pub struct BV {
43    term: BvTerm,
44    width: u32,
45}
46
47/// A boolean term (predicate).
48///
49/// Mirrors the `z3::ast::Bool` call surface used by the semantics encoders.
50#[derive(Clone, Debug)]
51pub struct Bool {
52    term: BoolTerm,
53}
54
55// ---------------------------------------------------------------------------
56// Structural ordering for canonicalization
57// ---------------------------------------------------------------------------
58
59fn bv_rank(t: &BvTerm) -> u8 {
60    match t {
61        BvTerm::Const { .. } => 0,
62        BvTerm::Var { .. } => 1,
63        BvTerm::Add(..) => 2,
64        BvTerm::Sub(..) => 3,
65        BvTerm::Mul(..) => 4,
66        BvTerm::Udiv(..) => 5,
67        // Urem MUST have its own rank: `ord_bv` asserts (via `unreachable!`)
68        // that rank-equal variants are exhaustively matched below, and there is
69        // no (Udiv, Urem) cross pair — sharing rank 5 made comparing a `Udiv`
70        // against a `Urem` panic. Reachable: the rem model is
71        // `rem = a − (a/b)·b`, so div and rem terms meet under a commutative op.
72        BvTerm::Urem(..) => 6,
73        BvTerm::And(..) => 7,
74        BvTerm::Or(..) => 8,
75        BvTerm::Xor(..) => 9,
76        BvTerm::Shl(..) => 10,
77        BvTerm::Lshr(..) => 11,
78        BvTerm::Ashr(..) => 12,
79        BvTerm::Rotr(..) => 13,
80        BvTerm::Extract { .. } => 14,
81        BvTerm::Concat(..) => 15,
82        BvTerm::ZeroExt { .. } => 16,
83        BvTerm::SignExt { .. } => 17,
84        BvTerm::Ite { .. } => 18,
85    }
86}
87
88fn bool_rank(t: &BoolTerm) -> u8 {
89    match t {
90        BoolTerm::Eq(..) => 0,
91        BoolTerm::Ne(..) => 1,
92        BoolTerm::Ult(..) => 2,
93        BoolTerm::Ule(..) => 3,
94        BoolTerm::Ugt(..) => 4,
95        BoolTerm::Uge(..) => 5,
96        BoolTerm::Slt(..) => 6,
97        BoolTerm::Sle(..) => 7,
98        BoolTerm::Sgt(..) => 8,
99        BoolTerm::Sge(..) => 9,
100        BoolTerm::Not(..) => 10,
101        BoolTerm::And(..) => 11,
102        BoolTerm::Or(..) => 12,
103    }
104}
105
106/// Total, deterministic structural order over `BvTerm` (canonicalization key).
107fn ord_bv(a: &BvTerm, b: &BvTerm) -> Ordering {
108    bv_rank(a).cmp(&bv_rank(b)).then_with(|| match (a, b) {
109        (
110            BvTerm::Const {
111                value: va,
112                sort: sa,
113            },
114            BvTerm::Const {
115                value: vb,
116                sort: sb,
117            },
118        ) => sa.width.cmp(&sb.width).then(va.cmp(vb)),
119        (BvTerm::Var { name: na, sort: sa }, BvTerm::Var { name: nb, sort: sb }) => {
120            sa.width.cmp(&sb.width).then_with(|| na.cmp(nb))
121        }
122        (BvTerm::Add(a1, a2), BvTerm::Add(b1, b2))
123        | (BvTerm::Sub(a1, a2), BvTerm::Sub(b1, b2))
124        | (BvTerm::Mul(a1, a2), BvTerm::Mul(b1, b2))
125        | (BvTerm::Udiv(a1, a2), BvTerm::Udiv(b1, b2))
126        | (BvTerm::Urem(a1, a2), BvTerm::Urem(b1, b2))
127        | (BvTerm::And(a1, a2), BvTerm::And(b1, b2))
128        | (BvTerm::Or(a1, a2), BvTerm::Or(b1, b2))
129        | (BvTerm::Xor(a1, a2), BvTerm::Xor(b1, b2))
130        | (BvTerm::Shl(a1, a2), BvTerm::Shl(b1, b2))
131        | (BvTerm::Lshr(a1, a2), BvTerm::Lshr(b1, b2))
132        | (BvTerm::Ashr(a1, a2), BvTerm::Ashr(b1, b2))
133        | (BvTerm::Rotr(a1, a2), BvTerm::Rotr(b1, b2))
134        | (BvTerm::Concat(a1, a2), BvTerm::Concat(b1, b2)) => {
135            ord_bv(a1, b1).then_with(|| ord_bv(a2, b2))
136        }
137        (
138            BvTerm::Extract {
139                hi: ha,
140                lo: la,
141                arg: aa,
142            },
143            BvTerm::Extract {
144                hi: hb,
145                lo: lb,
146                arg: ab,
147            },
148        ) => ha.cmp(hb).then(la.cmp(lb)).then_with(|| ord_bv(aa, ab)),
149        (BvTerm::ZeroExt { by: ba, arg: aa }, BvTerm::ZeroExt { by: bb, arg: ab })
150        | (BvTerm::SignExt { by: ba, arg: aa }, BvTerm::SignExt { by: bb, arg: ab }) => {
151            ba.cmp(bb).then_with(|| ord_bv(aa, ab))
152        }
153        (
154            BvTerm::Ite {
155                cond: ca,
156                then_: ta,
157                else_: ea,
158            },
159            BvTerm::Ite {
160                cond: cb,
161                then_: tb,
162                else_: eb,
163            },
164        ) => ord_bool(ca, cb)
165            .then_with(|| ord_bv(ta, tb))
166            .then_with(|| ord_bv(ea, eb)),
167        // Different ranks were handled above; same-rank pairs are all matched.
168        _ => unreachable!("ord_bv: rank-equal variants are exhaustively matched"),
169    })
170}
171
172/// Total, deterministic structural order over `BoolTerm`.
173fn ord_bool(a: &BoolTerm, b: &BoolTerm) -> Ordering {
174    bool_rank(a).cmp(&bool_rank(b)).then_with(|| match (a, b) {
175        (BoolTerm::Eq(a1, a2), BoolTerm::Eq(b1, b2))
176        | (BoolTerm::Ne(a1, a2), BoolTerm::Ne(b1, b2))
177        | (BoolTerm::Ult(a1, a2), BoolTerm::Ult(b1, b2))
178        | (BoolTerm::Ule(a1, a2), BoolTerm::Ule(b1, b2))
179        | (BoolTerm::Ugt(a1, a2), BoolTerm::Ugt(b1, b2))
180        | (BoolTerm::Uge(a1, a2), BoolTerm::Uge(b1, b2))
181        | (BoolTerm::Slt(a1, a2), BoolTerm::Slt(b1, b2))
182        | (BoolTerm::Sle(a1, a2), BoolTerm::Sle(b1, b2))
183        | (BoolTerm::Sgt(a1, a2), BoolTerm::Sgt(b1, b2))
184        | (BoolTerm::Sge(a1, a2), BoolTerm::Sge(b1, b2)) => {
185            ord_bv(a1, b1).then_with(|| ord_bv(a2, b2))
186        }
187        (BoolTerm::Not(a1), BoolTerm::Not(b1)) => ord_bool(a1, b1),
188        (BoolTerm::And(a1, a2), BoolTerm::And(b1, b2))
189        | (BoolTerm::Or(a1, a2), BoolTerm::Or(b1, b2)) => {
190            ord_bool(a1, b1).then_with(|| ord_bool(a2, b2))
191        }
192        _ => unreachable!("ord_bool: rank-equal variants are exhaustively matched"),
193    })
194}
195
196/// Order two operands of a commutative op deterministically (the interim
197/// canonicalization shim — see the module docs).
198fn canonical_pair(a: BvTerm, b: BvTerm) -> (Box<BvTerm>, Box<BvTerm>) {
199    if ord_bv(&a, &b) == Ordering::Greater {
200        (Box::new(b), Box::new(a))
201    } else {
202        (Box::new(a), Box::new(b))
203    }
204}
205
206/// Recursively canonicalize a term built *outside* the shim constructors
207/// (`ordeal::lowering` helpers build e.g. `Mul(q, b)` verbatim): reorder
208/// every commutative node bottom-up so shim-built and lowering-built
209/// expressions of the same value share one structure. Without this, the two
210/// sides of an equivalence query can differ by a commuted `Mul` — precisely
211/// the CDCL cliff the canonicalization exists to avoid.
212fn canonicalize_bv(t: &BvTerm) -> BvTerm {
213    let bin = |a: &BvTerm, b: &BvTerm| (Box::new(canonicalize_bv(a)), Box::new(canonicalize_bv(b)));
214    let comm = |a: &BvTerm, b: &BvTerm| canonical_pair(canonicalize_bv(a), canonicalize_bv(b));
215    match t {
216        BvTerm::Const { .. } | BvTerm::Var { .. } => t.clone(),
217        BvTerm::Add(a, b) => {
218            let (a, b) = comm(a, b);
219            BvTerm::Add(a, b)
220        }
221        BvTerm::Mul(a, b) => {
222            let (a, b) = comm(a, b);
223            BvTerm::Mul(a, b)
224        }
225        BvTerm::And(a, b) => {
226            let (a, b) = comm(a, b);
227            BvTerm::And(a, b)
228        }
229        BvTerm::Or(a, b) => {
230            let (a, b) = comm(a, b);
231            BvTerm::Or(a, b)
232        }
233        BvTerm::Xor(a, b) => {
234            let (a, b) = comm(a, b);
235            BvTerm::Xor(a, b)
236        }
237        BvTerm::Sub(a, b) => {
238            let (a, b) = bin(a, b);
239            BvTerm::Sub(a, b)
240        }
241        BvTerm::Udiv(a, b) => {
242            let (a, b) = bin(a, b);
243            BvTerm::Udiv(a, b)
244        }
245        BvTerm::Urem(a, b) => {
246            let (a, b) = bin(a, b);
247            BvTerm::Urem(a, b)
248        }
249        BvTerm::Shl(a, b) => {
250            let (a, b) = bin(a, b);
251            BvTerm::Shl(a, b)
252        }
253        BvTerm::Lshr(a, b) => {
254            let (a, b) = bin(a, b);
255            BvTerm::Lshr(a, b)
256        }
257        BvTerm::Ashr(a, b) => {
258            let (a, b) = bin(a, b);
259            BvTerm::Ashr(a, b)
260        }
261        BvTerm::Rotr(a, b) => {
262            let (a, b) = bin(a, b);
263            BvTerm::Rotr(a, b)
264        }
265        BvTerm::Concat(a, b) => {
266            let (a, b) = bin(a, b);
267            BvTerm::Concat(a, b)
268        }
269        BvTerm::Extract { hi, lo, arg } => BvTerm::Extract {
270            hi: *hi,
271            lo: *lo,
272            arg: Box::new(canonicalize_bv(arg)),
273        },
274        BvTerm::ZeroExt { by, arg } => BvTerm::ZeroExt {
275            by: *by,
276            arg: Box::new(canonicalize_bv(arg)),
277        },
278        BvTerm::SignExt { by, arg } => BvTerm::SignExt {
279            by: *by,
280            arg: Box::new(canonicalize_bv(arg)),
281        },
282        BvTerm::Ite { cond, then_, else_ } => BvTerm::Ite {
283            cond: Box::new(canonicalize_bool(cond)),
284            then_: Box::new(canonicalize_bv(then_)),
285            else_: Box::new(canonicalize_bv(else_)),
286        },
287    }
288}
289
290fn canonicalize_bool(t: &BoolTerm) -> BoolTerm {
291    let bin = |a: &BvTerm, b: &BvTerm| (Box::new(canonicalize_bv(a)), Box::new(canonicalize_bv(b)));
292    let comm = |a: &BvTerm, b: &BvTerm| canonical_pair(canonicalize_bv(a), canonicalize_bv(b));
293    match t {
294        BoolTerm::Eq(a, b) => {
295            let (a, b) = comm(a, b);
296            BoolTerm::Eq(a, b)
297        }
298        BoolTerm::Ne(a, b) => {
299            let (a, b) = comm(a, b);
300            BoolTerm::Ne(a, b)
301        }
302        BoolTerm::Ult(a, b) => {
303            let (a, b) = bin(a, b);
304            BoolTerm::Ult(a, b)
305        }
306        BoolTerm::Ule(a, b) => {
307            let (a, b) = bin(a, b);
308            BoolTerm::Ule(a, b)
309        }
310        BoolTerm::Ugt(a, b) => {
311            let (a, b) = bin(a, b);
312            BoolTerm::Ugt(a, b)
313        }
314        BoolTerm::Uge(a, b) => {
315            let (a, b) = bin(a, b);
316            BoolTerm::Uge(a, b)
317        }
318        BoolTerm::Slt(a, b) => {
319            let (a, b) = bin(a, b);
320            BoolTerm::Slt(a, b)
321        }
322        BoolTerm::Sle(a, b) => {
323            let (a, b) = bin(a, b);
324            BoolTerm::Sle(a, b)
325        }
326        BoolTerm::Sgt(a, b) => {
327            let (a, b) = bin(a, b);
328            BoolTerm::Sgt(a, b)
329        }
330        BoolTerm::Sge(a, b) => {
331            let (a, b) = bin(a, b);
332            BoolTerm::Sge(a, b)
333        }
334        BoolTerm::Not(a) => BoolTerm::Not(Box::new(canonicalize_bool(a))),
335        BoolTerm::And(a, b) => BoolTerm::And(
336            Box::new(canonicalize_bool(a)),
337            Box::new(canonicalize_bool(b)),
338        ),
339        BoolTerm::Or(a, b) => BoolTerm::Or(
340            Box::new(canonicalize_bool(a)),
341            Box::new(canonicalize_bool(b)),
342        ),
343    }
344}
345
346// ---------------------------------------------------------------------------
347// BV
348// ---------------------------------------------------------------------------
349
350impl BV {
351    /// The underlying ordeal term (consumed by the Z3 oracle translation).
352    #[cfg_attr(not(feature = "z3-solver"), allow(dead_code))]
353    pub(crate) fn term(&self) -> &BvTerm {
354        &self.term
355    }
356
357    /// Structural equality of the underlying terms. Used by the guarded
358    /// (branch-taking) ARM executor to skip vacuous `ite(g, x, x)` state
359    /// merges: without it every guarded instruction wraps ALL untouched
360    /// registers in a fresh `ite`, nesting the SDIV/UDIV operands in ite
361    /// chains and pushing the div/rem trap VC off a CDCL cliff (a genuinely
362    /// solver-hard miter of two perturbed division circuits).
363    pub(crate) fn same_term(&self, other: &BV) -> bool {
364        self.width == other.width && ord_bv(&self.term, &other.term) == Ordering::Equal
365    }
366
367    /// If this is a free variable, its name.
368    pub(crate) fn var_name(&self) -> Option<&str> {
369        match &self.term {
370            BvTerm::Var { name, .. } => Some(name),
371            _ => None,
372        }
373    }
374
375    /// A fresh free (symbolic) bitvector variable.
376    pub fn new_const(name: impl Into<String>, width: u32) -> Self {
377        Self {
378            term: BvTerm::Var {
379                name: name.into(),
380                sort: Sort::new(width),
381            },
382            width,
383        }
384    }
385
386    /// A concrete constant from a signed value (two's-complement, masked).
387    pub fn from_i64(value: i64, width: u32) -> Self {
388        Self {
389            term: BvTerm::Const {
390                value: (value as u64 as u128) & mask(width),
391                sort: Sort::new(width),
392            },
393            width,
394        }
395    }
396
397    /// A concrete constant from an unsigned value (masked to width).
398    pub fn from_u64(value: u64, width: u32) -> Self {
399        Self {
400            term: BvTerm::Const {
401                value: (value as u128) & mask(width),
402                sort: Sort::new(width),
403            },
404            width,
405        }
406    }
407
408    /// Bit width of this term.
409    pub fn get_size(&self) -> u32 {
410        self.width
411    }
412
413    fn binop(
414        &self,
415        other: impl Borrow<BV>,
416        f: impl FnOnce(Box<BvTerm>, Box<BvTerm>) -> BvTerm,
417    ) -> BV {
418        let other = other.borrow();
419        BV {
420            term: f(Box::new(self.term.clone()), Box::new(other.term.clone())),
421            width: self.width,
422        }
423    }
424
425    fn commutative(
426        &self,
427        other: impl Borrow<BV>,
428        f: impl FnOnce(Box<BvTerm>, Box<BvTerm>) -> BvTerm,
429    ) -> BV {
430        let other = other.borrow();
431        let (a, b) = canonical_pair(self.term.clone(), other.term.clone());
432        BV {
433            term: f(a, b),
434            width: self.width,
435        }
436    }
437
438    // --- Arithmetic ---
439
440    /// Modular addition.
441    pub fn bvadd(&self, other: impl Borrow<BV>) -> BV {
442        self.commutative(other, BvTerm::Add)
443    }
444
445    /// Modular subtraction.
446    pub fn bvsub(&self, other: impl Borrow<BV>) -> BV {
447        self.binop(other, BvTerm::Sub)
448    }
449
450    /// Modular multiplication.
451    pub fn bvmul(&self, other: impl Borrow<BV>) -> BV {
452        self.commutative(other, BvTerm::Mul)
453    }
454
455    /// Unsigned division (SMT-LIB: division by zero yields all-ones).
456    pub fn bvudiv(&self, other: impl Borrow<BV>) -> BV {
457        self.binop(other, BvTerm::Udiv)
458    }
459
460    /// Signed division (SMT-LIB semantics, via `ordeal::lowering`).
461    pub fn bvsdiv(&self, other: impl Borrow<BV>) -> BV {
462        BV {
463            term: canonicalize_bv(&ordeal::lowering::bvsdiv(
464                self.term.clone(),
465                other.borrow().term.clone(),
466                self.width,
467            )),
468            width: self.width,
469        }
470    }
471
472    /// Unsigned remainder (via `ordeal::lowering`).
473    pub fn bvurem(&self, other: impl Borrow<BV>) -> BV {
474        BV {
475            term: canonicalize_bv(&ordeal::lowering::bvurem(
476                self.term.clone(),
477                other.borrow().term.clone(),
478                self.width,
479            )),
480            width: self.width,
481        }
482    }
483
484    /// Signed remainder (via `ordeal::lowering`).
485    pub fn bvsrem(&self, other: impl Borrow<BV>) -> BV {
486        BV {
487            term: canonicalize_bv(&ordeal::lowering::bvsrem(
488                self.term.clone(),
489                other.borrow().term.clone(),
490                self.width,
491            )),
492            width: self.width,
493        }
494    }
495
496    /// Two's-complement negation (via `ordeal::lowering`).
497    pub fn bvneg(&self) -> BV {
498        BV {
499            term: canonicalize_bv(&ordeal::lowering::bvneg(self.term.clone(), self.width)),
500            width: self.width,
501        }
502    }
503
504    // --- Bitwise ---
505
506    /// Bitwise AND.
507    pub fn bvand(&self, other: impl Borrow<BV>) -> BV {
508        self.commutative(other, BvTerm::And)
509    }
510
511    /// Bitwise OR.
512    pub fn bvor(&self, other: impl Borrow<BV>) -> BV {
513        self.commutative(other, BvTerm::Or)
514    }
515
516    /// Bitwise XOR.
517    pub fn bvxor(&self, other: impl Borrow<BV>) -> BV {
518        self.commutative(other, BvTerm::Xor)
519    }
520
521    /// Bitwise NOT (via `ordeal::lowering`).
522    pub fn bvnot(&self) -> BV {
523        BV {
524            term: canonicalize_bv(&ordeal::lowering::bvnot(self.term.clone(), self.width)),
525            width: self.width,
526        }
527    }
528
529    // --- Shifts / rotates ---
530
531    /// Logical shift left (SMT-LIB oversize semantics: amount >= width → 0).
532    pub fn bvshl(&self, other: impl Borrow<BV>) -> BV {
533        self.binop(other, BvTerm::Shl)
534    }
535
536    /// Logical shift right.
537    pub fn bvlshr(&self, other: impl Borrow<BV>) -> BV {
538        self.binop(other, BvTerm::Lshr)
539    }
540
541    /// Arithmetic shift right.
542    pub fn bvashr(&self, other: impl Borrow<BV>) -> BV {
543        self.binop(other, BvTerm::Ashr)
544    }
545
546    /// Rotate left by a term amount (via `ordeal::lowering`).
547    pub fn bvrotl(&self, other: impl Borrow<BV>) -> BV {
548        BV {
549            term: canonicalize_bv(&ordeal::lowering::bvrotl(
550                self.term.clone(),
551                other.borrow().term.clone(),
552                self.width,
553            )),
554            width: self.width,
555        }
556    }
557
558    /// Rotate right by a term amount.
559    pub fn bvrotr(&self, other: impl Borrow<BV>) -> BV {
560        self.binop(other, BvTerm::Rotr)
561    }
562
563    // --- Structural ---
564
565    /// Bit extraction `[hi:lo]` (inclusive).
566    pub fn extract(&self, hi: u32, lo: u32) -> BV {
567        BV {
568            term: BvTerm::Extract {
569                hi,
570                lo,
571                arg: Box::new(self.term.clone()),
572            },
573            width: hi - lo + 1,
574        }
575    }
576
577    /// Concatenation: `self` becomes the high bits.
578    pub fn concat(&self, other: impl Borrow<BV>) -> BV {
579        let other = other.borrow();
580        BV {
581            term: BvTerm::Concat(Box::new(self.term.clone()), Box::new(other.term.clone())),
582            width: self.width + other.width,
583        }
584    }
585
586    /// Zero-extension by `by` bits.
587    pub fn zero_ext(&self, by: u32) -> BV {
588        BV {
589            term: BvTerm::ZeroExt {
590                by,
591                arg: Box::new(self.term.clone()),
592            },
593            width: self.width + by,
594        }
595    }
596
597    /// Sign-extension by `by` bits.
598    pub fn sign_ext(&self, by: u32) -> BV {
599        BV {
600            term: BvTerm::SignExt {
601                by,
602                arg: Box::new(self.term.clone()),
603            },
604            width: self.width + by,
605        }
606    }
607
608    // --- Predicates ---
609
610    fn cmp_op(
611        &self,
612        other: impl Borrow<BV>,
613        f: impl FnOnce(Box<BvTerm>, Box<BvTerm>) -> BoolTerm,
614    ) -> Bool {
615        Bool {
616            term: f(
617                Box::new(self.term.clone()),
618                Box::new(other.borrow().term.clone()),
619            ),
620        }
621    }
622
623    /// Equality predicate (canonicalized — `=` is commutative).
624    pub fn eq(&self, other: impl Borrow<BV>) -> Bool {
625        let (a, b) = canonical_pair(self.term.clone(), other.borrow().term.clone());
626        Bool {
627            term: BoolTerm::Eq(a, b),
628        }
629    }
630
631    /// Disequality predicate (canonicalized — `distinct` is commutative).
632    pub fn ne(&self, other: impl Borrow<BV>) -> Bool {
633        let (a, b) = canonical_pair(self.term.clone(), other.borrow().term.clone());
634        Bool {
635            term: BoolTerm::Ne(a, b),
636        }
637    }
638
639    /// Unsigned less-than.
640    pub fn bvult(&self, other: impl Borrow<BV>) -> Bool {
641        self.cmp_op(other, BoolTerm::Ult)
642    }
643
644    /// Unsigned less-or-equal.
645    pub fn bvule(&self, other: impl Borrow<BV>) -> Bool {
646        self.cmp_op(other, BoolTerm::Ule)
647    }
648
649    /// Unsigned greater-than.
650    pub fn bvugt(&self, other: impl Borrow<BV>) -> Bool {
651        self.cmp_op(other, BoolTerm::Ugt)
652    }
653
654    /// Unsigned greater-or-equal.
655    pub fn bvuge(&self, other: impl Borrow<BV>) -> Bool {
656        self.cmp_op(other, BoolTerm::Uge)
657    }
658
659    /// Signed less-than.
660    pub fn bvslt(&self, other: impl Borrow<BV>) -> Bool {
661        self.cmp_op(other, BoolTerm::Slt)
662    }
663
664    /// Signed less-or-equal.
665    pub fn bvsle(&self, other: impl Borrow<BV>) -> Bool {
666        self.cmp_op(other, BoolTerm::Sle)
667    }
668
669    /// Signed greater-than.
670    pub fn bvsgt(&self, other: impl Borrow<BV>) -> Bool {
671        self.cmp_op(other, BoolTerm::Sgt)
672    }
673
674    /// Signed greater-or-equal.
675    pub fn bvsge(&self, other: impl Borrow<BV>) -> Bool {
676        self.cmp_op(other, BoolTerm::Sge)
677    }
678
679    // --- Constant folding (test convenience; mirrors z3's simplify()) ---
680
681    /// Return `self` unchanged; pair with [`BV::as_i64`] / [`BV::as_u64`],
682    /// which concretely evaluate closed terms (the z3 idiom
683    /// `x.simplify().as_i64()` keeps working).
684    pub fn simplify(&self) -> BV {
685        self.clone()
686    }
687
688    /// Concrete value of a closed (variable-free) term, as z3's `as_i64`
689    /// reports it: the *unsigned* value when it fits in `i64`.
690    pub fn as_i64(&self) -> Option<i64> {
691        self.eval_closed().and_then(|v| i64::try_from(v).ok())
692    }
693
694    /// Concrete value of a closed (variable-free) term as `u64`.
695    pub fn as_u64(&self) -> Option<u64> {
696        self.eval_closed().and_then(|v| u64::try_from(v).ok())
697    }
698
699    fn eval_closed(&self) -> Option<u128> {
700        ordeal::eval::eval_bv(&self.term, &ordeal::eval::Env::new()).ok()
701    }
702}
703
704// ---------------------------------------------------------------------------
705// Bool
706// ---------------------------------------------------------------------------
707
708impl Bool {
709    /// The underlying ordeal term.
710    pub(crate) fn term(&self) -> &BoolTerm {
711        &self.term
712    }
713
714    /// Structural equality of the underlying terms (see [`BV::same_term`]).
715    pub(crate) fn same_term(&self, other: &Bool) -> bool {
716        ord_bool(&self.term, &other.term) == Ordering::Equal
717    }
718
719    /// Wrap a raw `ordeal::BoolTerm` — used by the trap module (`trap.rs`),
720    /// which builds predicates via `ordeal::trap::*` and needs to lift them
721    /// back into the crate's `Bool` (the field is private to this module).
722    pub(crate) fn from_ordeal(term: BoolTerm) -> Bool {
723        Bool { term }
724    }
725
726    /// A fresh free (symbolic) boolean variable.
727    ///
728    /// ordeal's fragment has no boolean variables, so this is encoded as
729    /// `var != 0` over a fresh 1-bit bitvector variable — an exact bridge.
730    pub fn new_const(name: impl Into<String>) -> Self {
731        let var = BvTerm::Var {
732            name: name.into(),
733            sort: Sort::new(1),
734        };
735        Self {
736            term: BoolTerm::Ne(
737                Box::new(var),
738                Box::new(BvTerm::Const {
739                    value: 0,
740                    sort: Sort::new(1),
741                }),
742            ),
743        }
744    }
745
746    /// The boolean literal `true` / `false`.
747    pub fn from_bool(value: bool) -> Self {
748        Self::literal(value)
749    }
750
751    /// Boolean equality (iff), encoded as `(a ∧ b) ∨ (¬a ∧ ¬b)` — the
752    /// fragment has no native boolean `=`.
753    pub fn eq(&self, other: impl Borrow<Bool>) -> Bool {
754        let a = self.term.clone();
755        let b = other.borrow().term.clone();
756        Bool {
757            term: BoolTerm::Or(
758                Box::new(BoolTerm::And(Box::new(a.clone()), Box::new(b.clone()))),
759                Box::new(BoolTerm::And(
760                    Box::new(BoolTerm::Not(Box::new(a))),
761                    Box::new(BoolTerm::Not(Box::new(b))),
762                )),
763            ),
764        }
765    }
766
767    /// Logical negation.
768    pub fn not(&self) -> Bool {
769        Bool {
770            term: BoolTerm::Not(Box::new(self.term.clone())),
771        }
772    }
773
774    /// N-ary conjunction (empty slice is `true`).
775    pub fn and(values: &[&Bool]) -> Bool {
776        Self::fold(values, BoolTerm::And, true)
777    }
778
779    /// N-ary disjunction (empty slice is `false`).
780    pub fn or(values: &[&Bool]) -> Bool {
781        Self::fold(values, BoolTerm::Or, false)
782    }
783
784    fn fold(
785        values: &[&Bool],
786        f: impl Fn(Box<BoolTerm>, Box<BoolTerm>) -> BoolTerm,
787        empty: bool,
788    ) -> Bool {
789        let mut iter = values.iter();
790        let Some(first) = iter.next() else {
791            return Self::literal(empty);
792        };
793        let mut acc = first.term.clone();
794        for v in iter {
795            acc = f(Box::new(acc), Box::new(v.term.clone()));
796        }
797        Bool { term: acc }
798    }
799
800    /// The boolean literal `true` / `false` (as a trivially decided
801    /// comparison — the fragment has no boolean constants).
802    fn literal(value: bool) -> Bool {
803        let zero = || {
804            Box::new(BvTerm::Const {
805                value: 0,
806                sort: Sort::new(1),
807            })
808        };
809        Bool {
810            term: if value {
811                BoolTerm::Eq(zero(), zero())
812            } else {
813                BoolTerm::Ne(zero(), zero())
814            },
815        }
816    }
817
818    /// If-then-else over bitvector branches (the bool→BV bridge;
819    /// `BvTerm::Ite` is native in ordeal 0.4).
820    pub fn ite(&self, then_: impl Borrow<BV>, else_: impl Borrow<BV>) -> BV {
821        let then_ = then_.borrow();
822        let else_ = else_.borrow();
823        BV {
824            term: BvTerm::Ite {
825                cond: Box::new(self.term.clone()),
826                then_: Box::new(then_.term.clone()),
827                else_: Box::new(else_.term.clone()),
828            },
829            width: then_.width,
830        }
831    }
832
833    /// Return `self` unchanged; pair with [`Bool::as_bool`], which concretely
834    /// evaluates closed predicates (the z3 `simplify().as_bool()` idiom).
835    pub fn simplify(&self) -> Bool {
836        self.clone()
837    }
838
839    /// Concrete value of a closed (variable-free) predicate.
840    pub fn as_bool(&self) -> Option<bool> {
841        ordeal::eval::eval_bool(&self.term, &ordeal::eval::Env::new()).ok()
842    }
843}
844
845// ---------------------------------------------------------------------------
846// Display (SMT-LIB-style mnemonics; used by tests and diagnostics)
847// ---------------------------------------------------------------------------
848
849fn fmt_bv(t: &BvTerm, f: &mut fmt::Formatter<'_>) -> fmt::Result {
850    match t {
851        BvTerm::Const { value, sort } => write!(f, "(_ bv{} {})", value, sort.width),
852        BvTerm::Var { name, .. } => write!(f, "{}", name),
853        BvTerm::Add(a, b) => fmt_bin(f, "bvadd", a, b),
854        BvTerm::Sub(a, b) => fmt_bin(f, "bvsub", a, b),
855        BvTerm::Mul(a, b) => fmt_bin(f, "bvmul", a, b),
856        BvTerm::Udiv(a, b) => fmt_bin(f, "bvudiv", a, b),
857        BvTerm::Urem(a, b) => fmt_bin(f, "bvurem", a, b),
858        BvTerm::And(a, b) => fmt_bin(f, "bvand", a, b),
859        BvTerm::Or(a, b) => fmt_bin(f, "bvor", a, b),
860        BvTerm::Xor(a, b) => fmt_bin(f, "bvxor", a, b),
861        BvTerm::Shl(a, b) => fmt_bin(f, "bvshl", a, b),
862        BvTerm::Lshr(a, b) => fmt_bin(f, "bvlshr", a, b),
863        BvTerm::Ashr(a, b) => fmt_bin(f, "bvashr", a, b),
864        BvTerm::Rotr(a, b) => fmt_bin(f, "bvrotr", a, b),
865        BvTerm::Extract { hi, lo, arg } => {
866            write!(f, "((_ extract {} {}) ", hi, lo)?;
867            fmt_bv(arg, f)?;
868            write!(f, ")")
869        }
870        BvTerm::Concat(a, b) => fmt_bin(f, "concat", a, b),
871        BvTerm::ZeroExt { by, arg } => {
872            write!(f, "((_ zero_extend {}) ", by)?;
873            fmt_bv(arg, f)?;
874            write!(f, ")")
875        }
876        BvTerm::SignExt { by, arg } => {
877            write!(f, "((_ sign_extend {}) ", by)?;
878            fmt_bv(arg, f)?;
879            write!(f, ")")
880        }
881        BvTerm::Ite { cond, then_, else_ } => {
882            write!(f, "(ite ")?;
883            fmt_bool(cond, f)?;
884            write!(f, " ")?;
885            fmt_bv(then_, f)?;
886            write!(f, " ")?;
887            fmt_bv(else_, f)?;
888            write!(f, ")")
889        }
890    }
891}
892
893fn fmt_bin(f: &mut fmt::Formatter<'_>, op: &str, a: &BvTerm, b: &BvTerm) -> fmt::Result {
894    write!(f, "({} ", op)?;
895    fmt_bv(a, f)?;
896    write!(f, " ")?;
897    fmt_bv(b, f)?;
898    write!(f, ")")
899}
900
901fn fmt_bool(t: &BoolTerm, f: &mut fmt::Formatter<'_>) -> fmt::Result {
902    let bin = |f: &mut fmt::Formatter<'_>, op: &str, a: &BvTerm, b: &BvTerm| -> fmt::Result {
903        write!(f, "({} ", op)?;
904        fmt_bv(a, f)?;
905        write!(f, " ")?;
906        fmt_bv(b, f)?;
907        write!(f, ")")
908    };
909    match t {
910        BoolTerm::Eq(a, b) => bin(f, "=", a, b),
911        BoolTerm::Ne(a, b) => bin(f, "distinct", a, b),
912        BoolTerm::Ult(a, b) => bin(f, "bvult", a, b),
913        BoolTerm::Ule(a, b) => bin(f, "bvule", a, b),
914        BoolTerm::Ugt(a, b) => bin(f, "bvugt", a, b),
915        BoolTerm::Uge(a, b) => bin(f, "bvuge", a, b),
916        BoolTerm::Slt(a, b) => bin(f, "bvslt", a, b),
917        BoolTerm::Sle(a, b) => bin(f, "bvsle", a, b),
918        BoolTerm::Sgt(a, b) => bin(f, "bvsgt", a, b),
919        BoolTerm::Sge(a, b) => bin(f, "bvsge", a, b),
920        BoolTerm::Not(a) => {
921            write!(f, "(not ")?;
922            fmt_bool(a, f)?;
923            write!(f, ")")
924        }
925        BoolTerm::And(a, b) => {
926            write!(f, "(and ")?;
927            fmt_bool(a, f)?;
928            write!(f, " ")?;
929            fmt_bool(b, f)?;
930            write!(f, ")")
931        }
932        BoolTerm::Or(a, b) => {
933            write!(f, "(or ")?;
934            fmt_bool(a, f)?;
935            write!(f, " ")?;
936            fmt_bool(b, f)?;
937            write!(f, ")")
938        }
939    }
940}
941
942impl fmt::Display for BV {
943    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
944        fmt_bv(&self.term, f)
945    }
946}
947
948impl fmt::Display for Bool {
949    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
950        fmt_bool(&self.term, f)
951    }
952}
953
954#[cfg(test)]
955mod tests {
956    use super::*;
957
958    #[test]
959    fn const_folding_matches_z3_idiom() {
960        let a = BV::from_i64(40, 32);
961        let b = BV::from_i64(2, 32);
962        assert_eq!(a.bvadd(&b).simplify().as_i64(), Some(42));
963        assert_eq!(a.bvsub(&b).simplify().as_i64(), Some(38));
964        // as_i64 reports the unsigned value (z3 behavior the tests rely on).
965        assert_eq!(BV::from_i64(-1, 32).as_i64(), Some(0xFFFF_FFFF));
966    }
967
968    #[test]
969    fn commutative_construction_is_canonical() {
970        let x = BV::new_const("x", 32);
971        let y = BV::new_const("y", 32);
972        // a*b and b*a must build the identical term (the v0.8.0 interim shim).
973        assert_eq!(x.bvmul(&y).to_string(), y.bvmul(&x).to_string());
974        assert_eq!(x.bvadd(&y).to_string(), y.bvadd(&x).to_string());
975        assert_eq!(x.eq(&y).to_string(), y.eq(&x).to_string());
976        // Non-commutative ops must NOT be reordered.
977        assert_ne!(x.bvsub(&y).to_string(), y.bvsub(&x).to_string());
978    }
979
980    #[test]
981    fn display_uses_smtlib_mnemonics() {
982        let x = BV::new_const("x", 32);
983        let s = x.bvadd(BV::from_i64(1, 32)).to_string();
984        assert!(s.contains("bvadd"), "{s}");
985    }
986}