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