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    /// If this is a free variable, its name.
347    pub(crate) fn var_name(&self) -> Option<&str> {
348        match &self.term {
349            BvTerm::Var { name, .. } => Some(name),
350            _ => None,
351        }
352    }
353
354    /// A fresh free (symbolic) bitvector variable.
355    pub fn new_const(name: impl Into<String>, width: u32) -> Self {
356        Self {
357            term: BvTerm::Var {
358                name: name.into(),
359                sort: Sort::new(width),
360            },
361            width,
362        }
363    }
364
365    /// A concrete constant from a signed value (two's-complement, masked).
366    pub fn from_i64(value: i64, width: u32) -> Self {
367        Self {
368            term: BvTerm::Const {
369                value: (value as u64 as u128) & mask(width),
370                sort: Sort::new(width),
371            },
372            width,
373        }
374    }
375
376    /// A concrete constant from an unsigned value (masked to width).
377    pub fn from_u64(value: u64, width: u32) -> Self {
378        Self {
379            term: BvTerm::Const {
380                value: (value as u128) & mask(width),
381                sort: Sort::new(width),
382            },
383            width,
384        }
385    }
386
387    /// Bit width of this term.
388    pub fn get_size(&self) -> u32 {
389        self.width
390    }
391
392    fn binop(
393        &self,
394        other: impl Borrow<BV>,
395        f: impl FnOnce(Box<BvTerm>, Box<BvTerm>) -> BvTerm,
396    ) -> BV {
397        let other = other.borrow();
398        BV {
399            term: f(Box::new(self.term.clone()), Box::new(other.term.clone())),
400            width: self.width,
401        }
402    }
403
404    fn commutative(
405        &self,
406        other: impl Borrow<BV>,
407        f: impl FnOnce(Box<BvTerm>, Box<BvTerm>) -> BvTerm,
408    ) -> BV {
409        let other = other.borrow();
410        let (a, b) = canonical_pair(self.term.clone(), other.term.clone());
411        BV {
412            term: f(a, b),
413            width: self.width,
414        }
415    }
416
417    // --- Arithmetic ---
418
419    /// Modular addition.
420    pub fn bvadd(&self, other: impl Borrow<BV>) -> BV {
421        self.commutative(other, BvTerm::Add)
422    }
423
424    /// Modular subtraction.
425    pub fn bvsub(&self, other: impl Borrow<BV>) -> BV {
426        self.binop(other, BvTerm::Sub)
427    }
428
429    /// Modular multiplication.
430    pub fn bvmul(&self, other: impl Borrow<BV>) -> BV {
431        self.commutative(other, BvTerm::Mul)
432    }
433
434    /// Unsigned division (SMT-LIB: division by zero yields all-ones).
435    pub fn bvudiv(&self, other: impl Borrow<BV>) -> BV {
436        self.binop(other, BvTerm::Udiv)
437    }
438
439    /// Signed division (SMT-LIB semantics, via `ordeal::lowering`).
440    pub fn bvsdiv(&self, other: impl Borrow<BV>) -> BV {
441        BV {
442            term: canonicalize_bv(&ordeal::lowering::bvsdiv(
443                self.term.clone(),
444                other.borrow().term.clone(),
445                self.width,
446            )),
447            width: self.width,
448        }
449    }
450
451    /// Unsigned remainder (via `ordeal::lowering`).
452    pub fn bvurem(&self, other: impl Borrow<BV>) -> BV {
453        BV {
454            term: canonicalize_bv(&ordeal::lowering::bvurem(
455                self.term.clone(),
456                other.borrow().term.clone(),
457                self.width,
458            )),
459            width: self.width,
460        }
461    }
462
463    /// Signed remainder (via `ordeal::lowering`).
464    pub fn bvsrem(&self, other: impl Borrow<BV>) -> BV {
465        BV {
466            term: canonicalize_bv(&ordeal::lowering::bvsrem(
467                self.term.clone(),
468                other.borrow().term.clone(),
469                self.width,
470            )),
471            width: self.width,
472        }
473    }
474
475    /// Two's-complement negation (via `ordeal::lowering`).
476    pub fn bvneg(&self) -> BV {
477        BV {
478            term: canonicalize_bv(&ordeal::lowering::bvneg(self.term.clone(), self.width)),
479            width: self.width,
480        }
481    }
482
483    // --- Bitwise ---
484
485    /// Bitwise AND.
486    pub fn bvand(&self, other: impl Borrow<BV>) -> BV {
487        self.commutative(other, BvTerm::And)
488    }
489
490    /// Bitwise OR.
491    pub fn bvor(&self, other: impl Borrow<BV>) -> BV {
492        self.commutative(other, BvTerm::Or)
493    }
494
495    /// Bitwise XOR.
496    pub fn bvxor(&self, other: impl Borrow<BV>) -> BV {
497        self.commutative(other, BvTerm::Xor)
498    }
499
500    /// Bitwise NOT (via `ordeal::lowering`).
501    pub fn bvnot(&self) -> BV {
502        BV {
503            term: canonicalize_bv(&ordeal::lowering::bvnot(self.term.clone(), self.width)),
504            width: self.width,
505        }
506    }
507
508    // --- Shifts / rotates ---
509
510    /// Logical shift left (SMT-LIB oversize semantics: amount >= width → 0).
511    pub fn bvshl(&self, other: impl Borrow<BV>) -> BV {
512        self.binop(other, BvTerm::Shl)
513    }
514
515    /// Logical shift right.
516    pub fn bvlshr(&self, other: impl Borrow<BV>) -> BV {
517        self.binop(other, BvTerm::Lshr)
518    }
519
520    /// Arithmetic shift right.
521    pub fn bvashr(&self, other: impl Borrow<BV>) -> BV {
522        self.binop(other, BvTerm::Ashr)
523    }
524
525    /// Rotate left by a term amount (via `ordeal::lowering`).
526    pub fn bvrotl(&self, other: impl Borrow<BV>) -> BV {
527        BV {
528            term: canonicalize_bv(&ordeal::lowering::bvrotl(
529                self.term.clone(),
530                other.borrow().term.clone(),
531                self.width,
532            )),
533            width: self.width,
534        }
535    }
536
537    /// Rotate right by a term amount.
538    pub fn bvrotr(&self, other: impl Borrow<BV>) -> BV {
539        self.binop(other, BvTerm::Rotr)
540    }
541
542    // --- Structural ---
543
544    /// Bit extraction `[hi:lo]` (inclusive).
545    pub fn extract(&self, hi: u32, lo: u32) -> BV {
546        BV {
547            term: BvTerm::Extract {
548                hi,
549                lo,
550                arg: Box::new(self.term.clone()),
551            },
552            width: hi - lo + 1,
553        }
554    }
555
556    /// Concatenation: `self` becomes the high bits.
557    pub fn concat(&self, other: impl Borrow<BV>) -> BV {
558        let other = other.borrow();
559        BV {
560            term: BvTerm::Concat(Box::new(self.term.clone()), Box::new(other.term.clone())),
561            width: self.width + other.width,
562        }
563    }
564
565    /// Zero-extension by `by` bits.
566    pub fn zero_ext(&self, by: u32) -> BV {
567        BV {
568            term: BvTerm::ZeroExt {
569                by,
570                arg: Box::new(self.term.clone()),
571            },
572            width: self.width + by,
573        }
574    }
575
576    /// Sign-extension by `by` bits.
577    pub fn sign_ext(&self, by: u32) -> BV {
578        BV {
579            term: BvTerm::SignExt {
580                by,
581                arg: Box::new(self.term.clone()),
582            },
583            width: self.width + by,
584        }
585    }
586
587    // --- Predicates ---
588
589    fn cmp_op(
590        &self,
591        other: impl Borrow<BV>,
592        f: impl FnOnce(Box<BvTerm>, Box<BvTerm>) -> BoolTerm,
593    ) -> Bool {
594        Bool {
595            term: f(
596                Box::new(self.term.clone()),
597                Box::new(other.borrow().term.clone()),
598            ),
599        }
600    }
601
602    /// Equality predicate (canonicalized — `=` is commutative).
603    pub fn eq(&self, other: impl Borrow<BV>) -> Bool {
604        let (a, b) = canonical_pair(self.term.clone(), other.borrow().term.clone());
605        Bool {
606            term: BoolTerm::Eq(a, b),
607        }
608    }
609
610    /// Disequality predicate (canonicalized — `distinct` is commutative).
611    pub fn ne(&self, other: impl Borrow<BV>) -> Bool {
612        let (a, b) = canonical_pair(self.term.clone(), other.borrow().term.clone());
613        Bool {
614            term: BoolTerm::Ne(a, b),
615        }
616    }
617
618    /// Unsigned less-than.
619    pub fn bvult(&self, other: impl Borrow<BV>) -> Bool {
620        self.cmp_op(other, BoolTerm::Ult)
621    }
622
623    /// Unsigned less-or-equal.
624    pub fn bvule(&self, other: impl Borrow<BV>) -> Bool {
625        self.cmp_op(other, BoolTerm::Ule)
626    }
627
628    /// Unsigned greater-than.
629    pub fn bvugt(&self, other: impl Borrow<BV>) -> Bool {
630        self.cmp_op(other, BoolTerm::Ugt)
631    }
632
633    /// Unsigned greater-or-equal.
634    pub fn bvuge(&self, other: impl Borrow<BV>) -> Bool {
635        self.cmp_op(other, BoolTerm::Uge)
636    }
637
638    /// Signed less-than.
639    pub fn bvslt(&self, other: impl Borrow<BV>) -> Bool {
640        self.cmp_op(other, BoolTerm::Slt)
641    }
642
643    /// Signed less-or-equal.
644    pub fn bvsle(&self, other: impl Borrow<BV>) -> Bool {
645        self.cmp_op(other, BoolTerm::Sle)
646    }
647
648    /// Signed greater-than.
649    pub fn bvsgt(&self, other: impl Borrow<BV>) -> Bool {
650        self.cmp_op(other, BoolTerm::Sgt)
651    }
652
653    /// Signed greater-or-equal.
654    pub fn bvsge(&self, other: impl Borrow<BV>) -> Bool {
655        self.cmp_op(other, BoolTerm::Sge)
656    }
657
658    // --- Constant folding (test convenience; mirrors z3's simplify()) ---
659
660    /// Return `self` unchanged; pair with [`BV::as_i64`] / [`BV::as_u64`],
661    /// which concretely evaluate closed terms (the z3 idiom
662    /// `x.simplify().as_i64()` keeps working).
663    pub fn simplify(&self) -> BV {
664        self.clone()
665    }
666
667    /// Concrete value of a closed (variable-free) term, as z3's `as_i64`
668    /// reports it: the *unsigned* value when it fits in `i64`.
669    pub fn as_i64(&self) -> Option<i64> {
670        self.eval_closed().and_then(|v| i64::try_from(v).ok())
671    }
672
673    /// Concrete value of a closed (variable-free) term as `u64`.
674    pub fn as_u64(&self) -> Option<u64> {
675        self.eval_closed().and_then(|v| u64::try_from(v).ok())
676    }
677
678    fn eval_closed(&self) -> Option<u128> {
679        ordeal::eval::eval_bv(&self.term, &ordeal::eval::Env::new()).ok()
680    }
681}
682
683// ---------------------------------------------------------------------------
684// Bool
685// ---------------------------------------------------------------------------
686
687impl Bool {
688    /// The underlying ordeal term.
689    pub(crate) fn term(&self) -> &BoolTerm {
690        &self.term
691    }
692
693    /// A fresh free (symbolic) boolean variable.
694    ///
695    /// ordeal's fragment has no boolean variables, so this is encoded as
696    /// `var != 0` over a fresh 1-bit bitvector variable — an exact bridge.
697    pub fn new_const(name: impl Into<String>) -> Self {
698        let var = BvTerm::Var {
699            name: name.into(),
700            sort: Sort::new(1),
701        };
702        Self {
703            term: BoolTerm::Ne(
704                Box::new(var),
705                Box::new(BvTerm::Const {
706                    value: 0,
707                    sort: Sort::new(1),
708                }),
709            ),
710        }
711    }
712
713    /// The boolean literal `true` / `false`.
714    pub fn from_bool(value: bool) -> Self {
715        Self::literal(value)
716    }
717
718    /// Boolean equality (iff), encoded as `(a ∧ b) ∨ (¬a ∧ ¬b)` — the
719    /// fragment has no native boolean `=`.
720    pub fn eq(&self, other: impl Borrow<Bool>) -> Bool {
721        let a = self.term.clone();
722        let b = other.borrow().term.clone();
723        Bool {
724            term: BoolTerm::Or(
725                Box::new(BoolTerm::And(Box::new(a.clone()), Box::new(b.clone()))),
726                Box::new(BoolTerm::And(
727                    Box::new(BoolTerm::Not(Box::new(a))),
728                    Box::new(BoolTerm::Not(Box::new(b))),
729                )),
730            ),
731        }
732    }
733
734    /// Logical negation.
735    pub fn not(&self) -> Bool {
736        Bool {
737            term: BoolTerm::Not(Box::new(self.term.clone())),
738        }
739    }
740
741    /// N-ary conjunction (empty slice is `true`).
742    pub fn and(values: &[&Bool]) -> Bool {
743        Self::fold(values, BoolTerm::And, true)
744    }
745
746    /// N-ary disjunction (empty slice is `false`).
747    pub fn or(values: &[&Bool]) -> Bool {
748        Self::fold(values, BoolTerm::Or, false)
749    }
750
751    fn fold(
752        values: &[&Bool],
753        f: impl Fn(Box<BoolTerm>, Box<BoolTerm>) -> BoolTerm,
754        empty: bool,
755    ) -> Bool {
756        let mut iter = values.iter();
757        let Some(first) = iter.next() else {
758            return Self::literal(empty);
759        };
760        let mut acc = first.term.clone();
761        for v in iter {
762            acc = f(Box::new(acc), Box::new(v.term.clone()));
763        }
764        Bool { term: acc }
765    }
766
767    /// The boolean literal `true` / `false` (as a trivially decided
768    /// comparison — the fragment has no boolean constants).
769    fn literal(value: bool) -> Bool {
770        let zero = || {
771            Box::new(BvTerm::Const {
772                value: 0,
773                sort: Sort::new(1),
774            })
775        };
776        Bool {
777            term: if value {
778                BoolTerm::Eq(zero(), zero())
779            } else {
780                BoolTerm::Ne(zero(), zero())
781            },
782        }
783    }
784
785    /// If-then-else over bitvector branches (the bool→BV bridge;
786    /// `BvTerm::Ite` is native in ordeal 0.4).
787    pub fn ite(&self, then_: impl Borrow<BV>, else_: impl Borrow<BV>) -> BV {
788        let then_ = then_.borrow();
789        let else_ = else_.borrow();
790        BV {
791            term: BvTerm::Ite {
792                cond: Box::new(self.term.clone()),
793                then_: Box::new(then_.term.clone()),
794                else_: Box::new(else_.term.clone()),
795            },
796            width: then_.width,
797        }
798    }
799
800    /// Return `self` unchanged; pair with [`Bool::as_bool`], which concretely
801    /// evaluates closed predicates (the z3 `simplify().as_bool()` idiom).
802    pub fn simplify(&self) -> Bool {
803        self.clone()
804    }
805
806    /// Concrete value of a closed (variable-free) predicate.
807    pub fn as_bool(&self) -> Option<bool> {
808        ordeal::eval::eval_bool(&self.term, &ordeal::eval::Env::new()).ok()
809    }
810}
811
812// ---------------------------------------------------------------------------
813// Display (SMT-LIB-style mnemonics; used by tests and diagnostics)
814// ---------------------------------------------------------------------------
815
816fn fmt_bv(t: &BvTerm, f: &mut fmt::Formatter<'_>) -> fmt::Result {
817    match t {
818        BvTerm::Const { value, sort } => write!(f, "(_ bv{} {})", value, sort.width),
819        BvTerm::Var { name, .. } => write!(f, "{}", name),
820        BvTerm::Add(a, b) => fmt_bin(f, "bvadd", a, b),
821        BvTerm::Sub(a, b) => fmt_bin(f, "bvsub", a, b),
822        BvTerm::Mul(a, b) => fmt_bin(f, "bvmul", a, b),
823        BvTerm::Udiv(a, b) => fmt_bin(f, "bvudiv", a, b),
824        BvTerm::And(a, b) => fmt_bin(f, "bvand", a, b),
825        BvTerm::Or(a, b) => fmt_bin(f, "bvor", a, b),
826        BvTerm::Xor(a, b) => fmt_bin(f, "bvxor", a, b),
827        BvTerm::Shl(a, b) => fmt_bin(f, "bvshl", a, b),
828        BvTerm::Lshr(a, b) => fmt_bin(f, "bvlshr", a, b),
829        BvTerm::Ashr(a, b) => fmt_bin(f, "bvashr", a, b),
830        BvTerm::Rotr(a, b) => fmt_bin(f, "bvrotr", a, b),
831        BvTerm::Extract { hi, lo, arg } => {
832            write!(f, "((_ extract {} {}) ", hi, lo)?;
833            fmt_bv(arg, f)?;
834            write!(f, ")")
835        }
836        BvTerm::Concat(a, b) => fmt_bin(f, "concat", a, b),
837        BvTerm::ZeroExt { by, arg } => {
838            write!(f, "((_ zero_extend {}) ", by)?;
839            fmt_bv(arg, f)?;
840            write!(f, ")")
841        }
842        BvTerm::SignExt { by, arg } => {
843            write!(f, "((_ sign_extend {}) ", by)?;
844            fmt_bv(arg, f)?;
845            write!(f, ")")
846        }
847        BvTerm::Ite { cond, then_, else_ } => {
848            write!(f, "(ite ")?;
849            fmt_bool(cond, f)?;
850            write!(f, " ")?;
851            fmt_bv(then_, f)?;
852            write!(f, " ")?;
853            fmt_bv(else_, f)?;
854            write!(f, ")")
855        }
856    }
857}
858
859fn fmt_bin(f: &mut fmt::Formatter<'_>, op: &str, a: &BvTerm, b: &BvTerm) -> fmt::Result {
860    write!(f, "({} ", op)?;
861    fmt_bv(a, f)?;
862    write!(f, " ")?;
863    fmt_bv(b, f)?;
864    write!(f, ")")
865}
866
867fn fmt_bool(t: &BoolTerm, f: &mut fmt::Formatter<'_>) -> fmt::Result {
868    let bin = |f: &mut fmt::Formatter<'_>, op: &str, a: &BvTerm, b: &BvTerm| -> fmt::Result {
869        write!(f, "({} ", op)?;
870        fmt_bv(a, f)?;
871        write!(f, " ")?;
872        fmt_bv(b, f)?;
873        write!(f, ")")
874    };
875    match t {
876        BoolTerm::Eq(a, b) => bin(f, "=", a, b),
877        BoolTerm::Ne(a, b) => bin(f, "distinct", a, b),
878        BoolTerm::Ult(a, b) => bin(f, "bvult", a, b),
879        BoolTerm::Ule(a, b) => bin(f, "bvule", a, b),
880        BoolTerm::Ugt(a, b) => bin(f, "bvugt", a, b),
881        BoolTerm::Uge(a, b) => bin(f, "bvuge", a, b),
882        BoolTerm::Slt(a, b) => bin(f, "bvslt", a, b),
883        BoolTerm::Sle(a, b) => bin(f, "bvsle", a, b),
884        BoolTerm::Sgt(a, b) => bin(f, "bvsgt", a, b),
885        BoolTerm::Sge(a, b) => bin(f, "bvsge", a, b),
886        BoolTerm::Not(a) => {
887            write!(f, "(not ")?;
888            fmt_bool(a, f)?;
889            write!(f, ")")
890        }
891        BoolTerm::And(a, b) => {
892            write!(f, "(and ")?;
893            fmt_bool(a, f)?;
894            write!(f, " ")?;
895            fmt_bool(b, f)?;
896            write!(f, ")")
897        }
898        BoolTerm::Or(a, b) => {
899            write!(f, "(or ")?;
900            fmt_bool(a, f)?;
901            write!(f, " ")?;
902            fmt_bool(b, f)?;
903            write!(f, ")")
904        }
905    }
906}
907
908impl fmt::Display for BV {
909    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
910        fmt_bv(&self.term, f)
911    }
912}
913
914impl fmt::Display for Bool {
915    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
916        fmt_bool(&self.term, f)
917    }
918}
919
920#[cfg(test)]
921mod tests {
922    use super::*;
923
924    #[test]
925    fn const_folding_matches_z3_idiom() {
926        let a = BV::from_i64(40, 32);
927        let b = BV::from_i64(2, 32);
928        assert_eq!(a.bvadd(&b).simplify().as_i64(), Some(42));
929        assert_eq!(a.bvsub(&b).simplify().as_i64(), Some(38));
930        // as_i64 reports the unsigned value (z3 behavior the tests rely on).
931        assert_eq!(BV::from_i64(-1, 32).as_i64(), Some(0xFFFF_FFFF));
932    }
933
934    #[test]
935    fn commutative_construction_is_canonical() {
936        let x = BV::new_const("x", 32);
937        let y = BV::new_const("y", 32);
938        // a*b and b*a must build the identical term (the v0.8.0 interim shim).
939        assert_eq!(x.bvmul(&y).to_string(), y.bvmul(&x).to_string());
940        assert_eq!(x.bvadd(&y).to_string(), y.bvadd(&x).to_string());
941        assert_eq!(x.eq(&y).to_string(), y.eq(&x).to_string());
942        // Non-commutative ops must NOT be reordered.
943        assert_ne!(x.bvsub(&y).to_string(), y.bvsub(&x).to_string());
944    }
945
946    #[test]
947    fn display_uses_smtlib_mnemonics() {
948        let x = BV::new_const("x", 32);
949        let s = x.bvadd(BV::from_i64(1, 32)).to_string();
950        assert!(s.contains("bvadd"), "{s}");
951    }
952}