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