Skip to main content

synth_verify/
validator_pattern.rs

1//! Validator-Pattern Verification (Issue #76)
2//!
3//! This module implements the **certifying-algorithm** verification pattern
4//! that CompCert uses for NP-hard passes (register allocation, instruction
5//! scheduling). Rather than verifying the *selector* — which is heuristic,
6//! large, and changes frequently — we verify a small *validator* that
7//! consumes the selector's output and confirms each concrete selection is
8//! semantically equivalent to the WASM op it replaces.
9//!
10//! See `docs/validator-pattern.md` for the architecture rationale.
11//!
12//! # Status
13//!
14//! The prototype landed by PR #113 covered only `WasmOp::I32Add`. This module
15//! extends the coverage to the **full i32 + i64 arithmetic / logic / shift /
16//! comparison surface** (issue #76). The pieces are:
17//!
18//! - [`CertifiedSelection`] — the per-op output of the selector
19//! - [`Validator`] — the trait the validator implements
20//! - [`Z3ArmValidator`] — a Z3-backed validator
21//!
22//! # How the validator works
23//!
24//! For each WASM op the validator builds a Z3 query that asserts the
25//! *negation* of equivalence between the WASM op's reference semantics and the
26//! ARM instruction sequence the selector emitted:
27//!
28//! ```text
29//! assert ¬( wasm_reference(inputs) == arm_lowering(inputs) )
30//! check-sat
31//! ```
32//!
33//! `unsat` means no input distinguishes the two — the selection is certified.
34//! `sat` means Z3 found a concrete counterexample — the selection is rejected.
35//!
36//! # Self-contained semantics
37//!
38//! The validator builds **both** the WASM reference and the ARM lowering
39//! semantics directly in this module. It deliberately does *not* delegate to
40//! [`crate::WasmSemantics`] / [`crate::ArmSemantics`] for i64: those encoders
41//! model i64 with truncated 32-bit semantics (see `wasm_semantics.rs` — every
42//! i64 op is a 32-bit stand-in), which is precisely the gap #76 closes. A
43//! validator that trusted a truncated model would certify wrong lowerings.
44//!
45//! Instead, every formula here is written in terms of 32-bit Z3 bitvectors —
46//! the same width the ARM machine actually has. i64 values are carried as a
47//! `(lo, hi)` pair of 32-bit bitvectors, exactly the register pair the ARM
48//! lowering uses. A reviewer can check each formula against the textbook
49//! 32-bit-limb algorithm for the corresponding 64-bit operation.
50//!
51//! # i64 register-pair modeling
52//!
53//! synth lowers a WASM i64 onto two ARM registers: `lo` (bits 0..=31) and `hi`
54//! (bits 32..=63). The validator never builds a 64-bit bitvector for the
55//! arithmetic/logic/shift surface. Instead, for each i64 op it builds the
56//! 64-bit result *as a pair of 32-bit limbs*, with carry / borrow propagated
57//! explicitly between the limbs:
58//!
59//! - **add**: `lo = a_lo + b_lo`, `carry = (lo <u a_lo)`,
60//!   `hi = a_hi + b_hi + carry`.
61//! - **sub**: `lo = a_lo - b_lo`, `borrow = (a_lo <u b_lo)`,
62//!   `hi = a_hi - b_hi - borrow`.
63//! - **mul**: schoolbook `a_lo*b_lo` plus the carry-out of that partial
64//!   product plus the cross terms `a_lo*b_hi + a_hi*b_lo` (mod 2^32).
65//! - **logic** (and/or/xor): limb-wise, no interaction between limbs.
66//! - **shifts**: case-split on whether the (mod-64) shift amount is `< 32` or
67//!   `>= 32`, with the cross-limb bit transfer made explicit.
68//! - **comparisons**: `hi` decides; `lo` breaks ties — written exactly so a
69//!   reviewer sees the lexicographic structure.
70//!
71//! Because the ARM lowering for an i64 op is itself a sequence of 32-bit ARM
72//! instructions, the validator compares the selector's emitted sequence
73//! limb-for-limb against this reference, and Z3 proves the two agree for all
74//! `2^64` inputs.
75//!
76//! # Scope
77//!
78//! Covered: i32 and i64 add / sub / mul / and / or / xor / shl / shr_s /
79//! shr_u / rotl / rotr; i32 and i64 eq / ne / lt_{s,u} / le_{s,u} /
80//! gt_{s,u} / ge_{s,u}; i32 and i64 eqz; i32 div_s / div_u.
81//!
82//! Scoped out, with reasons (see `docs/validator-pattern.md`):
83//!
84//! - **i32 rem_s / rem_u** — the ARM lowering is `SDIV`/`UDIV` followed by
85//!   `MLS`, i.e. the identity `r = a - (a / b) * b`. Proving this equals
86//!   WASM's `bvsrem` / `bvurem` for all 2^64 input pairs requires Z3 to
87//!   reason about a *symbolic* 32-bit multiply `(a / b) * b`, which
88//!   bit-blasts past any practical solver budget (no convergence in 5
89//!   minutes). `div_s` and `div_u` certify in well under a second — the
90//!   divide instruction maps straight onto `bvsdiv` / `bvudiv` with no
91//!   multiply — but *remainder* crosses the tractability line. Deferred
92//!   until the validator can discharge the `MLS` identity with a
93//!   multiplier-aware tactic rather than raw bit-blasting.
94//! - **i64 div_s / div_u / rem_s / rem_u** — synth lowers 64-bit division to
95//!   a *runtime-library call* (`__aeabi_ldivmod` and friends); there is no
96//!   fixed 32-bit ARM instruction sequence to symbolically execute. Modeling
97//!   a fictitious native 64-bit divide would certify a lowering the compiler
98//!   never emits. These are deferred until the validator can reason about
99//!   helper-call summaries.
100//! - **Clz / Ctz / Popcnt** (i32 and i64) — bit-counting. The 32-bit ARM
101//!   `CLZ` is available, but `Ctz`/`Popcnt` are lowered to multi-instruction
102//!   bit-twiddling and `i64.clz/ctz/popcnt` combine two limbs conditionally.
103//!   `wasm_semantics.rs` already has binary-search encoders for these; wiring
104//!   them through the validator's ARM-side executor is follow-up work and is
105//!   not part of the #76 arithmetic/logic/shift/comparison core.
106//! - **Extend8S / Extend16S** (and `i64` in-place extends) — sign-extension
107//!   ops; the ARM `SXTB`/`SXTH` lowering is straightforward but, like the
108//!   bit-counting ops, sits outside the #76 binary-op core and is deferred to
109//!   keep this change focused and reviewable.
110
111#![cfg(feature = "arm")]
112
113use crate::solver::{CheckOutcome, new_solver};
114use crate::term::{BV, Bool};
115use synth_core::WasmOp;
116use synth_synthesis::rules::Condition;
117use synth_synthesis::{ArmOp, Operand2, Reg};
118use thiserror::Error;
119
120/// A concrete selection produced by the instruction selector.
121///
122/// Generic over the source op `W` (initially `WasmOp`) and target op `A`
123/// (initially `ArmOp`, later `RiscvOp`). The `witness` is `None` when the
124/// selector emits the selection and `Some(Witness)` after the validator
125/// accepts it.
126#[derive(Debug, Clone)]
127pub struct CertifiedSelection<W, A> {
128    /// The source operation (e.g. `WasmOp::I32Add`).
129    pub wasm: W,
130    /// The target instruction sequence (e.g. `[ArmOp::Add { ... }]`).
131    pub arm: Vec<A>,
132    /// Witness attached by the validator (`None` until validated).
133    pub witness: Option<Witness>,
134}
135
136impl<W, A> CertifiedSelection<W, A> {
137    /// Create a new selection with no witness attached.
138    pub fn new(wasm: W, arm: Vec<A>) -> Self {
139        Self {
140            wasm,
141            arm,
142            witness: None,
143        }
144    }
145
146    /// Attach a witness, marking this selection as validated.
147    pub fn with_witness(mut self, witness: Witness) -> Self {
148        self.witness = Some(witness);
149        self
150    }
151
152    /// Whether this selection has been validated.
153    pub fn is_certified(&self) -> bool {
154        self.witness.is_some()
155    }
156}
157
158/// A witness recording the validator's acceptance of a selection.
159///
160/// For the prototype this is intentionally minimal. A v0.5 expansion will
161/// embed the SMT-LIB2 script that Z3 was given, so a third party can
162/// independently replay the verification without trusting the validator's
163/// encoding logic.
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct Witness {
166    /// Human-readable label for the WASM op (e.g. `"I32Add"`).
167    pub wasm_op_label: String,
168    /// Number of ARM instructions in the validated sequence.
169    pub arm_op_count: usize,
170    /// The Z3 result that produced this witness (always `Unsat` for accepted
171    /// selections — `Unsat` of `wasm ≠ arm` means `wasm ≡ arm`).
172    pub solver_result: SolverResultKind,
173}
174
175/// Solver result kind (mirrors [`crate::solver::CheckOutcome`]).
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub enum SolverResultKind {
178    /// `Unsat` of the negated equivalence — selection is correct.
179    Unsat,
180    /// `Sat` of the negated equivalence — counterexample found.
181    Sat,
182    /// Solver returned `unknown` (timeout or unsupported theory).
183    Unknown,
184}
185
186/// Validation failure reasons.
187#[derive(Debug, Error)]
188pub enum ValidationError {
189    /// The selection is semantically wrong; the validator found a
190    /// counterexample (a concrete input where WASM and ARM disagree).
191    #[error("counterexample for {wasm_op_label}: {description}")]
192    Counterexample {
193        wasm_op_label: String,
194        description: String,
195    },
196
197    /// The validator does not support this WASM op. This is the common case
198    /// during the per-op rollout (see `docs/validator-pattern.md`).
199    #[error("validator does not support {0:?}")]
200    UnsupportedOp(WasmOp),
201
202    /// Z3 returned `unknown` (timeout or unsupported theory).
203    #[error("solver returned unknown: {0}")]
204    SolverUnknown(String),
205
206    /// Internal encoding error (e.g. the emitted ARM sequence uses an
207    /// instruction the validator's lowering model does not cover).
208    #[error("internal validator error: {0}")]
209    Internal(String),
210}
211
212/// A backend-agnostic validator for [`CertifiedSelection`].
213///
214/// Implementations consume a selection and either return a [`Witness`]
215/// (selection is correct) or a [`ValidationError`] (selection is wrong, or
216/// unsupported, or inconclusive).
217pub trait Validator<W, A> {
218    /// Validate `sel`. Returns `Ok(Witness)` iff the selection is
219    /// semantically equivalent to the source op.
220    fn validate(&self, sel: &CertifiedSelection<W, A>) -> Result<Witness, ValidationError>;
221}
222
223// ===========================================================================
224// Bitvector helpers — everything works on 32-bit bitvectors, the width the
225// ARM Cortex-M machine has. i64 values are carried as a `(lo, hi)` limb pair.
226// ===========================================================================
227
228/// Make a fresh 32-bit symbolic input.
229fn sym32(name: &str) -> BV {
230    BV::new_const(name, 32)
231}
232
233/// 32-bit constant.
234fn k32(v: i64) -> BV {
235    BV::from_i64(v, 32)
236}
237
238/// Boolean → 32-bit `0`/`1` bitvector (WASM comparison result encoding).
239fn bool_to_i32(b: &Bool) -> BV {
240    b.ite(k32(1), k32(0))
241}
242
243/// 64-bit value as a pair of 32-bit limbs: `lo` is bits 0..=31, `hi` is
244/// bits 32..=63 — exactly the ARM register pair the i64 lowering uses.
245#[derive(Clone)]
246struct I64Pair {
247    lo: BV,
248    hi: BV,
249}
250
251impl I64Pair {
252    /// Bit-for-bit equality: both limbs must agree.
253    fn eq_pair(&self, other: &I64Pair) -> Bool {
254        Bool::and(&[&self.lo.eq(&other.lo), &self.hi.eq(&other.hi)])
255    }
256
257    /// Join the limb pair into a single 64-bit bitvector.
258    ///
259    /// `concat(hi, lo)` places `hi` in bits 32..=63 and `lo` in bits 0..=31 —
260    /// the standard little-endian-limb layout the ARM register pair uses.
261    /// Used by the multiply reference, where a native 64-bit `bvmul` is far
262    /// clearer (and far easier for Z3) than a hand-rolled limb product.
263    fn join64(&self) -> BV {
264        self.hi.concat(&self.lo)
265    }
266
267    /// Inverse of [`I64Pair::join64`]: split a 64-bit bitvector into limbs.
268    fn from_u64_bv(v: &BV) -> I64Pair {
269        I64Pair {
270            lo: v.extract(31, 0),
271            hi: v.extract(63, 32),
272        }
273    }
274}
275
276// ===========================================================================
277// 64-bit reference semantics, expressed in 32-bit limbs
278// ===========================================================================
279//
280// Each function below is the *reference* meaning of a WASM i64 op, written so
281// a reviewer can match it against the textbook 32-bit-limb algorithm. No
282// 64-bit bitvector is ever constructed — the limb pair *is* the value.
283
284/// 64-bit add: ripple-carry across the two 32-bit limbs.
285///
286/// `lo = a_lo + b_lo`. A carry out of the low limb happens exactly when the
287/// 32-bit sum wraps, i.e. when `lo <u a_lo` (unsigned). The carry (0 or 1) is
288/// then folded into the high limb: `hi = a_hi + b_hi + carry`.
289fn i64_add(a: &I64Pair, b: &I64Pair) -> I64Pair {
290    let lo = a.lo.bvadd(&b.lo);
291    let carry = lo.bvult(&a.lo).ite(k32(1), k32(0));
292    let hi = a.hi.bvadd(&b.hi).bvadd(&carry);
293    I64Pair { lo, hi }
294}
295
296/// 64-bit sub: ripple-borrow across the two 32-bit limbs.
297///
298/// `lo = a_lo - b_lo`. A borrow into the high limb happens exactly when the
299/// low subtraction underflows, i.e. when `a_lo <u b_lo`. The borrow is then
300/// subtracted from the high limb: `hi = a_hi - b_hi - borrow`.
301fn i64_sub(a: &I64Pair, b: &I64Pair) -> I64Pair {
302    let lo = a.lo.bvsub(&b.lo);
303    let borrow = a.lo.bvult(&b.lo).ite(k32(1), k32(0));
304    let hi = a.hi.bvsub(&b.hi).bvsub(&borrow);
305    I64Pair { lo, hi }
306}
307
308/// 64-bit multiply, low 64 bits only (WASM `i64.mul` wraps mod 2^64).
309///
310/// The two limb pairs are joined into native 64-bit bitvectors, multiplied
311/// with Z3's `bvmul` (which is exactly mod-2^64 multiplication), and split
312/// back into limbs. Using the native 64-bit multiply keeps the reference
313/// *obviously* correct — it is literally the 64-bit product — and avoids a
314/// hand-rolled partial-product expansion that is both harder to audit and far
315/// harder for the SMT solver to reason about. The ARM-side `I64Mul` lowering
316/// (the real UMULL + MLA cross-product expansion) is checked against this
317/// reference.
318fn i64_mul(a: &I64Pair, b: &I64Pair) -> I64Pair {
319    I64Pair::from_u64_bv(&a.join64().bvmul(b.join64()))
320}
321
322/// Effective i64 shift amount: WASM masks the shift count to `count mod 64`,
323/// i.e. the low 6 bits of the second operand's low limb.
324fn shift_amount64(b: &I64Pair) -> BV {
325    b.lo.bvand(k32(63))
326}
327
328/// 64-bit logical shift left by a symbolic amount `s` (already reduced mod
329/// 64), in 32-bit limbs.
330///
331/// Case split:
332///
333/// * `s == 0`: identity.
334/// * `0 < s < 32`: `hi = (hi << s) | (lo >>u (32-s))`, `lo = lo << s`.
335/// * `s >= 32`: `hi = lo << (s-32)`, `lo = 0`.
336///
337/// The middle case is the only one with a cross-limb transfer; `32 - s` is
338/// well-defined there because `0 < s < 32`.
339fn i64_shl(a: &I64Pair, s: &BV) -> I64Pair {
340    let s_lt_32 = s.bvult(k32(32));
341    let s_is_zero = s.eq(k32(0));
342
343    let lo_small = a.lo.bvshl(s);
344    let hi_small = a.hi.bvshl(s).bvor(a.lo.bvlshr(k32(32).bvsub(s)));
345
346    let lo_big = k32(0);
347    let hi_big = a.lo.bvshl(s.bvsub(k32(32)));
348
349    let lo = s_is_zero.ite(&a.lo, s_lt_32.ite(&lo_small, &lo_big));
350    let hi = s_is_zero.ite(&a.hi, s_lt_32.ite(&hi_small, &hi_big));
351    I64Pair { lo, hi }
352}
353
354/// 64-bit logical (unsigned) shift right — mirror image of [`i64_shl`]:
355///
356/// * `s == 0`: identity.
357/// * `0 < s < 32`: `lo = (lo >>u s) | (hi << (32-s))`, `hi = hi >>u s`.
358/// * `s >= 32`: `lo = hi >>u (s-32)`, `hi = 0`.
359fn i64_shr_u(a: &I64Pair, s: &BV) -> I64Pair {
360    let s_lt_32 = s.bvult(k32(32));
361    let s_is_zero = s.eq(k32(0));
362
363    let lo_small = a.lo.bvlshr(s).bvor(a.hi.bvshl(k32(32).bvsub(s)));
364    let hi_small = a.hi.bvlshr(s);
365
366    let lo_big = a.hi.bvlshr(s.bvsub(k32(32)));
367    let hi_big = k32(0);
368
369    let lo = s_is_zero.ite(&a.lo, s_lt_32.ite(&lo_small, &lo_big));
370    let hi = s_is_zero.ite(&a.hi, s_lt_32.ite(&hi_small, &hi_big));
371    I64Pair { lo, hi }
372}
373
374/// 64-bit arithmetic (signed) shift right — same structure as [`i64_shr_u`],
375/// but the high limb is filled with the sign bit instead of zero:
376///
377/// * `s == 0`: identity.
378/// * `0 < s < 32`: `lo = (lo >>u s) | (hi << (32-s))`, `hi = hi >>s s`.
379/// * `s >= 32`: `lo = hi >>s (s-32)`, `hi = sign-fill of hi`.
380///
381/// `sign` is all-ones when `hi` is negative, all-zeros otherwise — obtained
382/// by an arithmetic shift of `hi` right by 31.
383fn i64_shr_s(a: &I64Pair, s: &BV) -> I64Pair {
384    let s_lt_32 = s.bvult(k32(32));
385    let s_is_zero = s.eq(k32(0));
386    let sign = a.hi.bvashr(k32(31)); // 0x0000_0000 or 0xFFFF_FFFF
387
388    let lo_small = a.lo.bvlshr(s).bvor(a.hi.bvshl(k32(32).bvsub(s)));
389    let hi_small = a.hi.bvashr(s);
390
391    let lo_big = a.hi.bvashr(s.bvsub(k32(32)));
392    let hi_big = sign.clone();
393
394    let lo = s_is_zero.ite(&a.lo, s_lt_32.ite(&lo_small, &lo_big));
395    let hi = s_is_zero.ite(&a.hi, s_lt_32.ite(&hi_small, &hi_big));
396    I64Pair { lo, hi }
397}
398
399/// 64-bit rotate left: `rotl(x, s) = (x << s) | (x >>u (64-s))`.
400///
401/// `(64 - s) mod 64` keeps the right-shift amount in range and maps `s == 0`
402/// to a 0 shift, so the OR below reduces to the identity at `s == 0`.
403fn i64_rotl(a: &I64Pair, s: &BV) -> I64Pair {
404    let left = i64_shl(a, s);
405    let comp = k32(64).bvsub(s).bvand(k32(63));
406    let right = i64_shr_u(a, &comp);
407    I64Pair {
408        lo: left.lo.bvor(&right.lo),
409        hi: left.hi.bvor(&right.hi),
410    }
411}
412
413/// 64-bit rotate right: `rotr(x, s) = (x >>u s) | (x << (64-s))`.
414fn i64_rotr(a: &I64Pair, s: &BV) -> I64Pair {
415    let right = i64_shr_u(a, s);
416    let comp = k32(64).bvsub(s).bvand(k32(63));
417    let left = i64_shl(a, &comp);
418    I64Pair {
419        lo: right.lo.bvor(&left.lo),
420        hi: right.hi.bvor(&left.hi),
421    }
422}
423
424/// 64-bit unsigned less-than over the limb pair.
425///
426/// Lexicographic: the high limbs decide; if they are equal the low limbs
427/// (compared unsigned) break the tie. `a <u b  ⇔  a_hi <u b_hi ∨
428/// (a_hi == b_hi ∧ a_lo <u b_lo)`.
429fn i64_lt_u(a: &I64Pair, b: &I64Pair) -> Bool {
430    let hi_lt = a.hi.bvult(&b.hi);
431    let hi_eq = a.hi.eq(&b.hi);
432    let lo_lt = a.lo.bvult(&b.lo);
433    Bool::or(&[&hi_lt, &Bool::and(&[&hi_eq, &lo_lt])])
434}
435
436/// 64-bit signed less-than over the limb pair.
437///
438/// Identical to the unsigned case except the *high* limbs are compared
439/// **signed** (the sign of the 64-bit value lives in the top bit of `hi`).
440/// The low limbs are still compared unsigned — they carry no sign of their
441/// own.
442fn i64_lt_s(a: &I64Pair, b: &I64Pair) -> Bool {
443    let hi_lt = a.hi.bvslt(&b.hi);
444    let hi_eq = a.hi.eq(&b.hi);
445    let lo_lt = a.lo.bvult(&b.lo);
446    Bool::or(&[&hi_lt, &Bool::and(&[&hi_eq, &lo_lt])])
447}
448
449// ===========================================================================
450// ARM NZCV condition flags
451// ===========================================================================
452
453/// The four ARM condition flags, as Z3 booleans.
454#[derive(Clone)]
455struct Flags {
456    n: Bool, // Negative — result bit 31
457    z: Bool, // Zero
458    c: Bool, // Carry / no-borrow
459    v: Bool, // Signed overflow
460}
461
462impl Flags {
463    /// Flags before any compare has run — unconstrained.
464    fn unconstrained() -> Self {
465        Self {
466            n: Bool::new_const("flag_n"),
467            z: Bool::new_const("flag_z"),
468            c: Bool::new_const("flag_c"),
469            v: Bool::new_const("flag_v"),
470        }
471    }
472
473    /// Flags produced by an ARM compare of `a` against `b` (i.e. `a - b`).
474    ///
475    /// This is the standard ARM `CMP` flag update:
476    ///   * `N` — bit 31 of `a - b`.
477    ///   * `Z` — `a == b`.
478    ///   * `C` — *no borrow*, i.e. `a >=u b` (ARM carry convention for SUB).
479    ///   * `V` — signed overflow of `a - b`: the operands have different
480    ///     signs and the result's sign differs from `a`'s.
481    fn from_cmp(a: &BV, b: &BV) -> Self {
482        let result = a.bvsub(b);
483        let n = result.bvslt(k32(0));
484        let z = a.eq(b);
485        let c = a.bvuge(b);
486        // Signed overflow: sign(a) != sign(b) && sign(result) != sign(a).
487        let a_neg = a.bvslt(k32(0));
488        let b_neg = b.bvslt(k32(0));
489        let r_neg = result.bvslt(k32(0));
490        let signs_differ = a_neg.eq(&b_neg).not();
491        let result_sign_wrong = r_neg.eq(&a_neg).not();
492        let v = Bool::and(&[&signs_differ, &result_sign_wrong]);
493        Self { n, z, c, v }
494    }
495
496    /// Evaluate an ARM [`Condition`] against these flags.
497    ///
498    /// Standard ARM condition-code semantics; see the `Condition` enum doc.
499    fn holds(&self, cond: &Condition) -> Bool {
500        match cond {
501            Condition::EQ => self.z.clone(),
502            Condition::NE => self.z.not(),
503            // signed
504            Condition::LT => self.n.eq(&self.v).not(),
505            Condition::GE => self.n.eq(&self.v),
506            Condition::LE => Bool::or(&[&self.z, &self.n.eq(&self.v).not()]),
507            Condition::GT => Bool::and(&[&self.z.not(), &self.n.eq(&self.v)]),
508            // unsigned
509            Condition::LO => self.c.not(),
510            Condition::HS => self.c.clone(),
511            Condition::LS => Bool::or(&[&self.c.not(), &self.z.clone()]),
512            Condition::HI => Bool::and(&[&self.c, &self.z.not()]),
513        }
514    }
515}
516
517// ===========================================================================
518// Z3-backed validator
519// ===========================================================================
520
521/// What an op's result looks like: a single 32-bit value, or an i64 limb
522/// pair.
523enum OpResult {
524    /// A 32-bit value (i32 ops, and i64 comparisons / `eqz`).
525    Word(BV),
526    /// A 64-bit value as `(lo, hi)` limbs (i64 arithmetic / logic / shift).
527    Pair(I64Pair),
528}
529
530/// Z3-backed validator for the WASM → ARM selection pattern.
531///
532/// For each call the validator:
533///
534/// 1. Allocates symbolic 32-bit inputs (a limb pair per i64 operand).
535/// 2. Builds the WASM op's reference result from those inputs.
536/// 3. Builds the ARM sequence's result by symbolically executing it.
537/// 4. Asserts the *negation* of equivalence and checks satisfiability:
538///    `unsat` ⇒ equivalent for all inputs (accept), `sat` ⇒ counterexample
539///    (reject).
540///
541/// The validator is **trusted** code: it is the small, auditable piece that
542/// replaces ~150 per-op Rocq proofs. Every formula it builds is 32-bit
543/// bitvector arithmetic that a reviewer can check by hand.
544pub struct Z3ArmValidator;
545
546impl Default for Z3ArmValidator {
547    fn default() -> Self {
548        Self::new()
549    }
550}
551
552impl Z3ArmValidator {
553    /// Construct a fresh validator. Callers should wrap any sequence of
554    /// `validate` calls in [`crate::with_verification_context`] (a no-op for
555    /// the default ordeal engine; configures Z3's thread-local context when
556    /// the differential oracle is compiled in).
557    pub fn new() -> Self {
558        Self
559    }
560
561    /// Build the WASM op's reference result from symbolic inputs.
562    ///
563    /// `a` and `b` are the two operands as limb pairs. For i32 ops only the
564    /// `.lo` limb is meaningful. Returns `None` for ops outside the supported
565    /// surface.
566    fn wasm_reference(&self, op: &WasmOp, a: &I64Pair, b: &I64Pair) -> Option<OpResult> {
567        use WasmOp::*;
568        let word = |bv: BV| Some(OpResult::Word(bv));
569        let pair = |p: I64Pair| Some(OpResult::Pair(p));
570        match op {
571            // --- i32 arithmetic --------------------------------------------
572            I32Add => word(a.lo.bvadd(&b.lo)),
573            I32Sub => word(a.lo.bvsub(&b.lo)),
574            I32Mul => word(a.lo.bvmul(&b.lo)),
575            // --- i32 logic -------------------------------------------------
576            I32And => word(a.lo.bvand(&b.lo)),
577            I32Or => word(a.lo.bvor(&b.lo)),
578            I32Xor => word(a.lo.bvxor(&b.lo)),
579            // --- i32 shifts (WASM masks the count mod 32) ------------------
580            I32Shl => word(a.lo.bvshl(b.lo.bvand(k32(31)))),
581            I32ShrU => word(a.lo.bvlshr(b.lo.bvand(k32(31)))),
582            I32ShrS => word(a.lo.bvashr(b.lo.bvand(k32(31)))),
583            I32Rotl => word(a.lo.bvrotl(b.lo.bvand(k32(31)))),
584            I32Rotr => word(a.lo.bvrotr(b.lo.bvand(k32(31)))),
585            // --- i32 div (precondition excludes the trap inputs) -----------
586            // i32.rem_s / rem_u are scoped out — see module docs (the MLS
587            // remainder identity is SMT-intractable: the symbolic multiply
588            // bit-blasts past any practical solver budget).
589            I32DivS => word(a.lo.bvsdiv(&b.lo)),
590            I32DivU => word(a.lo.bvudiv(&b.lo)),
591            // --- i32 comparisons (result is 0/1) ---------------------------
592            I32Eq => word(bool_to_i32(&a.lo.eq(&b.lo))),
593            I32Ne => word(bool_to_i32(&a.lo.eq(&b.lo).not())),
594            I32LtS => word(bool_to_i32(&a.lo.bvslt(&b.lo))),
595            I32LtU => word(bool_to_i32(&a.lo.bvult(&b.lo))),
596            I32LeS => word(bool_to_i32(&a.lo.bvsle(&b.lo))),
597            I32LeU => word(bool_to_i32(&a.lo.bvule(&b.lo))),
598            I32GtS => word(bool_to_i32(&a.lo.bvsgt(&b.lo))),
599            I32GtU => word(bool_to_i32(&a.lo.bvugt(&b.lo))),
600            I32GeS => word(bool_to_i32(&a.lo.bvsge(&b.lo))),
601            I32GeU => word(bool_to_i32(&a.lo.bvuge(&b.lo))),
602            // --- i32 unary -------------------------------------------------
603            I32Eqz => word(bool_to_i32(&a.lo.eq(k32(0)))),
604            // --- i64 arithmetic --------------------------------------------
605            I64Add => pair(i64_add(a, b)),
606            I64Sub => pair(i64_sub(a, b)),
607            I64Mul => pair(i64_mul(a, b)),
608            // --- i64 logic -------------------------------------------------
609            I64And => pair(I64Pair {
610                lo: a.lo.bvand(&b.lo),
611                hi: a.hi.bvand(&b.hi),
612            }),
613            I64Or => pair(I64Pair {
614                lo: a.lo.bvor(&b.lo),
615                hi: a.hi.bvor(&b.hi),
616            }),
617            I64Xor => pair(I64Pair {
618                lo: a.lo.bvxor(&b.lo),
619                hi: a.hi.bvxor(&b.hi),
620            }),
621            // --- i64 shifts (WASM masks the count mod 64) ------------------
622            I64Shl => pair(i64_shl(a, &shift_amount64(b))),
623            I64ShrU => pair(i64_shr_u(a, &shift_amount64(b))),
624            I64ShrS => pair(i64_shr_s(a, &shift_amount64(b))),
625            I64Rotl => pair(i64_rotl(a, &shift_amount64(b))),
626            I64Rotr => pair(i64_rotr(a, &shift_amount64(b))),
627            // --- i64 comparisons (result is a 32-bit 0/1) ------------------
628            I64Eq => word(bool_to_i32(&a.eq_pair(b))),
629            I64Ne => word(bool_to_i32(&a.eq_pair(b).not())),
630            I64LtU => word(bool_to_i32(&i64_lt_u(a, b))),
631            I64LtS => word(bool_to_i32(&i64_lt_s(a, b))),
632            I64GtU => word(bool_to_i32(&i64_lt_u(b, a))),
633            I64GtS => word(bool_to_i32(&i64_lt_s(b, a))),
634            I64LeU => word(bool_to_i32(&i64_lt_u(b, a).not())),
635            I64LeS => word(bool_to_i32(&i64_lt_s(b, a).not())),
636            I64GeU => word(bool_to_i32(&i64_lt_u(a, b).not())),
637            I64GeS => word(bool_to_i32(&i64_lt_s(a, b).not())),
638            // --- i64 unary -------------------------------------------------
639            I64Eqz => word(bool_to_i32(&Bool::and(&[
640                &a.lo.eq(k32(0)),
641                &a.hi.eq(k32(0)),
642            ]))),
643            // i64 div/rem are scoped out — see module docs.
644            _ => None,
645        }
646    }
647
648    /// Number of stack operands a supported WASM op consumes.
649    /// `None` means the op is outside the supported surface.
650    fn arity(&self, op: &WasmOp) -> Option<usize> {
651        use WasmOp::*;
652        match op {
653            I32Add | I32Sub | I32Mul | I32And | I32Or | I32Xor | I32Shl | I32ShrU | I32ShrS
654            | I32Rotl | I32Rotr | I32DivS | I32DivU | I32Eq | I32Ne | I32LtS | I32LtU | I32LeS
655            | I32LeU | I32GtS | I32GtU | I32GeS | I32GeU | I64Add | I64Sub | I64Mul | I64And
656            | I64Or | I64Xor | I64Shl | I64ShrU | I64ShrS | I64Rotl | I64Rotr | I64Eq | I64Ne
657            | I64LtU | I64LtS | I64GtU | I64GtS | I64LeU | I64LeS | I64GeU | I64GeS => Some(2),
658            I32Eqz | I64Eqz => Some(1),
659            _ => None,
660        }
661    }
662
663    /// Whether the op takes i64 operands (and therefore seeds register
664    /// pairs).
665    fn is_i64(op: &WasmOp) -> bool {
666        use WasmOp::*;
667        matches!(
668            op,
669            I64Add
670                | I64Sub
671                | I64Mul
672                | I64And
673                | I64Or
674                | I64Xor
675                | I64Shl
676                | I64ShrU
677                | I64ShrS
678                | I64Rotl
679                | I64Rotr
680                | I64Eq
681                | I64Ne
682                | I64LtU
683                | I64LtS
684                | I64GtU
685                | I64GtS
686                | I64LeU
687                | I64LeS
688                | I64GeU
689                | I64GeS
690                | I64Eqz
691        )
692    }
693
694    /// Whether the op's *result* is a 64-bit limb pair (vs a 32-bit word).
695    /// i64 comparisons and `i64.eqz` return an i32, so they are *not* pairs.
696    fn result_is_pair(op: &WasmOp) -> bool {
697        use WasmOp::*;
698        matches!(
699            op,
700            I64Add
701                | I64Sub
702                | I64Mul
703                | I64And
704                | I64Or
705                | I64Xor
706                | I64Shl
707                | I64ShrU
708                | I64ShrS
709                | I64Rotl
710                | I64Rotr
711        )
712    }
713
714    /// Whether the op traps on certain inputs (division).
715    fn is_div_rem(op: &WasmOp) -> bool {
716        use WasmOp::*;
717        matches!(op, I32DivS | I32DivU)
718    }
719
720    /// Trap-avoidance precondition for division ops.
721    ///
722    /// WASM `div` traps (rather than producing a value) when:
723    ///   * the divisor is zero, or
724    ///   * (signed only) the dividend is `INT_MIN` and the divisor is `-1`,
725    ///     which overflows the result.
726    ///
727    /// The validator certifies the **non-trapping** behaviour: it asserts the
728    /// precondition so Z3 only considers inputs on which WASM actually
729    /// produces a value. The trap path itself (the selector's `CMP/BNE/UDF`
730    /// guard) is control flow, validated separately at the CFG level — see
731    /// `docs/validator-pattern.md`. This keeps the div query honest (the
732    /// value-domain equivalence is real) without overclaiming.
733    fn div_rem_precondition(&self, op: &WasmOp, a: &I64Pair, b: &I64Pair) -> Bool {
734        use WasmOp::*;
735        match op {
736            I32DivU => b.lo.eq(k32(0)).not(),
737            I32DivS => {
738                let div_nonzero = b.lo.eq(k32(0)).not();
739                let int_min = a.lo.eq(k32(i32::MIN as i64));
740                let neg_one = b.lo.eq(k32(-1));
741                let overflow = Bool::and(&[&int_min, &neg_one]);
742                Bool::and(&[&div_nonzero, &overflow.not()])
743            }
744            _ => Bool::from_bool(true),
745        }
746    }
747
748    /// Symbolically execute the emitted ARM sequence and return the result.
749    ///
750    /// The inputs are placed in the ARM registers the selector's calling
751    /// convention uses:
752    ///
753    /// * i32 ops: operand 0 → R0, operand 1 → R1.
754    /// * i64 ops: operand 0 → (R0 = lo, R1 = hi), operand 1 → (R2 = lo, R3 = hi).
755    ///
756    /// The result is read back from R0 (i32 ops and i64 comparisons) or the
757    /// (R0, R1) pair (i64 arithmetic / logic / shift).
758    ///
759    /// Only the small ARM-op subset the i32/i64 lowerings actually use is
760    /// modeled. An op outside that subset is a [`ValidationError::Internal`]
761    /// — the validator refuses to silently certify an instruction it cannot
762    /// reason about.
763    fn execute_arm(
764        &self,
765        op: &WasmOp,
766        arm: &[ArmOp],
767        a: &I64Pair,
768        b: &I64Pair,
769    ) -> Result<OpResult, ValidationError> {
770        // 16 general-purpose registers, all symbolic except the seeded
771        // inputs.
772        let mut regs: Vec<BV> = (0..16).map(|i| sym32(&format!("r{i}"))).collect();
773        let mut flags = Flags::unconstrained();
774
775        let is64 = Self::is_i64(op);
776        let arity = self.arity(op).unwrap_or(0);
777
778        // Seed the input registers per the calling convention above.
779        if is64 {
780            regs[0] = a.lo.clone();
781            regs[1] = a.hi.clone();
782            if arity == 2 {
783                regs[2] = b.lo.clone();
784                regs[3] = b.hi.clone();
785            }
786        } else {
787            regs[0] = a.lo.clone();
788            if arity == 2 {
789                regs[1] = b.lo.clone();
790            }
791        }
792
793        let idx = reg_index;
794
795        // Resolve an `Operand2` against the current register file.
796        let eval_op2 = |regs: &[BV], op2: &Operand2| -> Result<BV, ValidationError> {
797            match op2 {
798                Operand2::Reg(r) => Ok(regs[idx(r)].clone()),
799                Operand2::Imm(v) => Ok(k32(*v as i64)),
800                other => Err(ValidationError::Internal(format!(
801                    "unsupported Operand2 in lowering: {other:?}"
802                ))),
803            }
804        };
805
806        for instr in arm {
807            match instr {
808                ArmOp::Add { rd, rn, op2 } => {
809                    regs[idx(rd)] = regs[idx(rn)].bvadd(&eval_op2(&regs, op2)?);
810                }
811                ArmOp::Sub { rd, rn, op2 } => {
812                    regs[idx(rd)] = regs[idx(rn)].bvsub(&eval_op2(&regs, op2)?);
813                }
814                // ADDS: add and set the carry flag (i64 low limb).
815                ArmOp::Adds { rd, rn, op2 } => {
816                    let rn_v = regs[idx(rn)].clone();
817                    let v = rn_v.bvadd(&eval_op2(&regs, op2)?);
818                    // Unsigned carry out: the 32-bit sum wrapped below rn.
819                    flags.c = v.bvult(&rn_v);
820                    regs[idx(rd)] = v;
821                }
822                // ADC: add with the carry flag (i64 high limb).
823                ArmOp::Adc { rd, rn, op2 } => {
824                    let c = flags.c.ite(k32(1), k32(0));
825                    regs[idx(rd)] = regs[idx(rn)].bvadd(&eval_op2(&regs, op2)?).bvadd(&c);
826                }
827                // SUBS: subtract and set carry. ARM convention: C = 1 means
828                // *no* borrow (rn >=u op2).
829                ArmOp::Subs { rd, rn, op2 } => {
830                    let rn_v = regs[idx(rn)].clone();
831                    let op2_v = eval_op2(&regs, op2)?;
832                    flags.c = rn_v.bvuge(&op2_v);
833                    regs[idx(rd)] = rn_v.bvsub(&op2_v);
834                }
835                // SBC: subtract with borrow. Rd = Rn - op2 - (1 - C).
836                ArmOp::Sbc { rd, rn, op2 } => {
837                    let borrow = flags.c.ite(k32(0), k32(1));
838                    regs[idx(rd)] = regs[idx(rn)].bvsub(&eval_op2(&regs, op2)?).bvsub(&borrow);
839                }
840                ArmOp::Mul { rd, rn, rm } => {
841                    regs[idx(rd)] = regs[idx(rn)].bvmul(&regs[idx(rm)]);
842                }
843                ArmOp::And { rd, rn, op2 } => {
844                    regs[idx(rd)] = regs[idx(rn)].bvand(&eval_op2(&regs, op2)?);
845                }
846                ArmOp::Orr { rd, rn, op2 } => {
847                    regs[idx(rd)] = regs[idx(rn)].bvor(&eval_op2(&regs, op2)?);
848                }
849                ArmOp::Eor { rd, rn, op2 } => {
850                    regs[idx(rd)] = regs[idx(rn)].bvxor(&eval_op2(&regs, op2)?);
851                }
852                ArmOp::Mov { rd, op2 } => {
853                    regs[idx(rd)] = eval_op2(&regs, op2)?;
854                }
855                ArmOp::Mvn { rd, op2 } => {
856                    regs[idx(rd)] = eval_op2(&regs, op2)?.bvnot();
857                }
858                // Register-amount shifts. Z3's bvshl/bvlshr/bvashr are total
859                // for any amount; the selector is responsible for masking the
860                // amount mod 32, and an unmasked lowering would fail the
861                // equivalence check.
862                ArmOp::LslReg { rd, rn, rm } => {
863                    regs[idx(rd)] = regs[idx(rn)].bvshl(&regs[idx(rm)]);
864                }
865                ArmOp::LsrReg { rd, rn, rm } => {
866                    regs[idx(rd)] = regs[idx(rn)].bvlshr(&regs[idx(rm)]);
867                }
868                ArmOp::AsrReg { rd, rn, rm } => {
869                    regs[idx(rd)] = regs[idx(rn)].bvashr(&regs[idx(rm)]);
870                }
871                ArmOp::RorReg { rd, rn, rm } => {
872                    regs[idx(rd)] = regs[idx(rn)].bvrotr(&regs[idx(rm)]);
873                }
874                ArmOp::Lsl { rd, rn, shift } => {
875                    regs[idx(rd)] = regs[idx(rn)].bvshl(k32(*shift as i64));
876                }
877                ArmOp::Lsr { rd, rn, shift } => {
878                    regs[idx(rd)] = regs[idx(rn)].bvlshr(k32(*shift as i64));
879                }
880                ArmOp::Asr { rd, rn, shift } => {
881                    regs[idx(rd)] = regs[idx(rn)].bvashr(k32(*shift as i64));
882                }
883                ArmOp::Ror { rd, rn, shift } => {
884                    regs[idx(rd)] = regs[idx(rn)].bvrotr(k32(*shift as i64));
885                }
886                // Rsb: reverse subtract — Rd = imm - Rn (used by rotl as
887                // 0 - shift to get the negated rotate amount).
888                ArmOp::Rsb { rd, rn, imm } => {
889                    regs[idx(rd)] = k32(*imm as i64).bvsub(&regs[idx(rn)]);
890                }
891                ArmOp::Sdiv { rd, rn, rm } => {
892                    regs[idx(rd)] = regs[idx(rn)].bvsdiv(&regs[idx(rm)]);
893                }
894                ArmOp::Udiv { rd, rn, rm } => {
895                    regs[idx(rd)] = regs[idx(rn)].bvudiv(&regs[idx(rm)]);
896                }
897                // MLS: Rd = Ra - Rn*Rm — the remainder idiom `a - (a/b)*b`.
898                ArmOp::Mls { rd, rn, rm, ra } => {
899                    regs[idx(rd)] = regs[idx(ra)].bvsub(regs[idx(rn)].bvmul(&regs[idx(rm)]));
900                }
901                // CMP: set NZCV from Rn - op2; no register write.
902                ArmOp::Cmp { rn, op2 } => {
903                    let rn_v = regs[idx(rn)].clone();
904                    let op2_v = eval_op2(&regs, op2)?;
905                    flags = Flags::from_cmp(&rn_v, &op2_v);
906                }
907                // SetCond: materialize an NZCV condition into Rd as 0/1.
908                // This is the verification pseudo-op the selector emits when
909                // a comparison result is consumed as a value (rather than by
910                // a conditional branch). It is the value-domain face of the
911                // "CMP-only" lowering in issue #73 item 2.
912                ArmOp::SetCond { rd, cond } => {
913                    regs[idx(rd)] = bool_to_i32(&flags.holds(cond));
914                }
915                // I64SetCond: compare a register pair (rn_lo:rn_hi) against
916                // (rm_lo:rm_hi) and materialize the condition as 0/1 in rd.
917                // The validator models it directly against the i64 reference
918                // comparison — see the lexicographic `i64_lt_*` helpers.
919                ArmOp::I64SetCond {
920                    rd,
921                    rn_lo,
922                    rn_hi,
923                    rm_lo,
924                    rm_hi,
925                    cond,
926                } => {
927                    let n = I64Pair {
928                        lo: regs[idx(rn_lo)].clone(),
929                        hi: regs[idx(rn_hi)].clone(),
930                    };
931                    let m = I64Pair {
932                        lo: regs[idx(rm_lo)].clone(),
933                        hi: regs[idx(rm_hi)].clone(),
934                    };
935                    let holds = match cond {
936                        Condition::EQ => n.eq_pair(&m),
937                        Condition::NE => n.eq_pair(&m).not(),
938                        Condition::LT => i64_lt_s(&n, &m),
939                        Condition::GE => i64_lt_s(&n, &m).not(),
940                        Condition::GT => i64_lt_s(&m, &n),
941                        Condition::LE => i64_lt_s(&m, &n).not(),
942                        Condition::LO => i64_lt_u(&n, &m),
943                        Condition::HS => i64_lt_u(&n, &m).not(),
944                        Condition::HI => i64_lt_u(&m, &n),
945                        Condition::LS => i64_lt_u(&m, &n).not(),
946                    };
947                    regs[idx(rd)] = bool_to_i32(&holds);
948                }
949                // I64Mul: 64-bit multiply of register pairs (rn_lo:rn_hi) and
950                // (rm_lo:rm_hi), low 64 bits of the product into
951                // (rd_lo:rd_hi). The real selector expands this to
952                // UMULL + MLA cross products; the validator checks the
953                // composite pseudo-op against the schoolbook `i64_mul`
954                // reference (carry of the low partial product + cross terms).
955                ArmOp::I64Mul {
956                    rd_lo,
957                    rd_hi,
958                    rn_lo,
959                    rn_hi,
960                    rm_lo,
961                    rm_hi,
962                } => {
963                    let n = I64Pair {
964                        lo: regs[idx(rn_lo)].clone(),
965                        hi: regs[idx(rn_hi)].clone(),
966                    };
967                    let m = I64Pair {
968                        lo: regs[idx(rm_lo)].clone(),
969                        hi: regs[idx(rm_hi)].clone(),
970                    };
971                    let prod = i64_mul(&n, &m);
972                    regs[idx(rd_lo)] = prod.lo;
973                    regs[idx(rd_hi)] = prod.hi;
974                }
975                // I64Shl / I64ShrU / I64ShrS: 64-bit shift of a register
976                // pair by an amount in (rm_lo). The real selector expands
977                // each to a multi-instruction sequence with a runtime branch
978                // on whether the (mod-64) amount is < 32 or >= 32; the
979                // validator checks the composite pseudo-op against the
980                // `i64_shl` / `i64_shr_u` / `i64_shr_s` references, which
981                // model exactly that case split. The shift amount is masked
982                // mod 64, matching WASM's `count mod 64` rule.
983                ArmOp::I64Shl {
984                    rd_lo,
985                    rd_hi,
986                    rn_lo,
987                    rn_hi,
988                    rm_lo,
989                    rm_hi: _,
990                } => {
991                    let n = I64Pair {
992                        lo: regs[idx(rn_lo)].clone(),
993                        hi: regs[idx(rn_hi)].clone(),
994                    };
995                    let amt = regs[idx(rm_lo)].bvand(k32(63));
996                    let r = i64_shl(&n, &amt);
997                    regs[idx(rd_lo)] = r.lo;
998                    regs[idx(rd_hi)] = r.hi;
999                }
1000                ArmOp::I64ShrU {
1001                    rd_lo,
1002                    rd_hi,
1003                    rn_lo,
1004                    rn_hi,
1005                    rm_lo,
1006                    rm_hi: _,
1007                } => {
1008                    let n = I64Pair {
1009                        lo: regs[idx(rn_lo)].clone(),
1010                        hi: regs[idx(rn_hi)].clone(),
1011                    };
1012                    let amt = regs[idx(rm_lo)].bvand(k32(63));
1013                    let r = i64_shr_u(&n, &amt);
1014                    regs[idx(rd_lo)] = r.lo;
1015                    regs[idx(rd_hi)] = r.hi;
1016                }
1017                ArmOp::I64ShrS {
1018                    rd_lo,
1019                    rd_hi,
1020                    rn_lo,
1021                    rn_hi,
1022                    rm_lo,
1023                    rm_hi: _,
1024                } => {
1025                    let n = I64Pair {
1026                        lo: regs[idx(rn_lo)].clone(),
1027                        hi: regs[idx(rn_hi)].clone(),
1028                    };
1029                    let amt = regs[idx(rm_lo)].bvand(k32(63));
1030                    let r = i64_shr_s(&n, &amt);
1031                    regs[idx(rd_lo)] = r.lo;
1032                    regs[idx(rd_hi)] = r.hi;
1033                }
1034                // I64Rotl / I64Rotr: 64-bit rotate of a register pair by an
1035                // amount held in a single register. Checked against the
1036                // `i64_rotl` / `i64_rotr` references, which are themselves
1037                // built from the shift primitives. The amount is masked
1038                // mod 64, matching WASM's `count mod 64` rule.
1039                ArmOp::I64Rotl {
1040                    rdlo,
1041                    rdhi,
1042                    rnlo,
1043                    rnhi,
1044                    shift,
1045                } => {
1046                    let n = I64Pair {
1047                        lo: regs[idx(rnlo)].clone(),
1048                        hi: regs[idx(rnhi)].clone(),
1049                    };
1050                    let amt = regs[idx(shift)].bvand(k32(63));
1051                    let r = i64_rotl(&n, &amt);
1052                    regs[idx(rdlo)] = r.lo;
1053                    regs[idx(rdhi)] = r.hi;
1054                }
1055                ArmOp::I64Rotr {
1056                    rdlo,
1057                    rdhi,
1058                    rnlo,
1059                    rnhi,
1060                    shift,
1061                } => {
1062                    let n = I64Pair {
1063                        lo: regs[idx(rnlo)].clone(),
1064                        hi: regs[idx(rnhi)].clone(),
1065                    };
1066                    let amt = regs[idx(shift)].bvand(k32(63));
1067                    let r = i64_rotr(&n, &amt);
1068                    regs[idx(rdlo)] = r.lo;
1069                    regs[idx(rdhi)] = r.hi;
1070                }
1071                // I64SetCondZ: 0/1 in rd iff the register pair is all-zero.
1072                ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
1073                    let is_zero =
1074                        Bool::and(&[&regs[idx(rn_lo)].eq(k32(0)), &regs[idx(rn_hi)].eq(k32(0))]);
1075                    regs[idx(rd)] = bool_to_i32(&is_zero);
1076                }
1077                ArmOp::Nop => {}
1078                other => {
1079                    return Err(ValidationError::Internal(format!(
1080                        "ARM op not modeled by validator: {other:?}"
1081                    )));
1082                }
1083            }
1084        }
1085
1086        if is64 && Self::result_is_pair(op) {
1087            Ok(OpResult::Pair(I64Pair {
1088                lo: regs[0].clone(),
1089                hi: regs[1].clone(),
1090            }))
1091        } else {
1092            Ok(OpResult::Word(regs[0].clone()))
1093        }
1094    }
1095}
1096
1097/// Map a `Reg` to its 0..=15 index.
1098fn reg_index(r: &Reg) -> usize {
1099    match r {
1100        Reg::R0 => 0,
1101        Reg::R1 => 1,
1102        Reg::R2 => 2,
1103        Reg::R3 => 3,
1104        Reg::R4 => 4,
1105        Reg::R5 => 5,
1106        Reg::R6 => 6,
1107        Reg::R7 => 7,
1108        Reg::R8 => 8,
1109        Reg::R9 => 9,
1110        Reg::R10 => 10,
1111        Reg::R11 => 11,
1112        Reg::R12 => 12,
1113        Reg::SP => 13,
1114        Reg::LR => 14,
1115        Reg::PC => 15,
1116    }
1117}
1118
1119impl Validator<WasmOp, ArmOp> for Z3ArmValidator {
1120    fn validate(
1121        &self,
1122        sel: &CertifiedSelection<WasmOp, ArmOp>,
1123    ) -> Result<Witness, ValidationError> {
1124        let label = format!("{:?}", sel.wasm);
1125
1126        // Gate: is this op in the supported surface at all?
1127        self.arity(&sel.wasm)
1128            .ok_or_else(|| ValidationError::UnsupportedOp(sel.wasm.clone()))?;
1129
1130        let mut solver = new_solver();
1131
1132        // Symbolic operands. Each operand is a limb pair; for i32 ops only the
1133        // `.lo` limb is meaningful and the `.hi` limb is left unconstrained
1134        // (it is never read by the i32 reference or the i32 lowering).
1135        let a = I64Pair {
1136            lo: sym32("a_lo"),
1137            hi: sym32("a_hi"),
1138        };
1139        let b = I64Pair {
1140            lo: sym32("b_lo"),
1141            hi: sym32("b_hi"),
1142        };
1143
1144        // For division / remainder, restrict to non-trapping inputs so the
1145        // value-domain equivalence is the property actually being proved.
1146        if Self::is_div_rem(&sel.wasm) {
1147            solver.assert(&self.div_rem_precondition(&sel.wasm, &a, &b));
1148        }
1149
1150        let wasm_result = self
1151            .wasm_reference(&sel.wasm, &a, &b)
1152            .ok_or_else(|| ValidationError::UnsupportedOp(sel.wasm.clone()))?;
1153        let arm_result = self.execute_arm(&sel.wasm, &sel.arm, &a, &b)?;
1154
1155        // Assert ¬(wasm == arm); unsat ⇒ equivalent for all inputs.
1156        let differ = match (&wasm_result, &arm_result) {
1157            (OpResult::Word(w), OpResult::Word(r)) => w.eq(r).not(),
1158            (OpResult::Pair(w), OpResult::Pair(r)) => w.eq_pair(r).not(),
1159            _ => {
1160                return Err(ValidationError::Internal(format!(
1161                    "result-shape mismatch for {label}: WASM and ARM disagree on word vs pair"
1162                )));
1163            }
1164        };
1165        solver.assert(&differ);
1166
1167        match solver.check() {
1168            CheckOutcome::Unsat => Ok(Witness {
1169                wasm_op_label: label,
1170                arm_op_count: sel.arm.len(),
1171                solver_result: SolverResultKind::Unsat,
1172            }),
1173            CheckOutcome::Sat => {
1174                // Model readback: report the differing inputs.
1175                let mut parts = Vec::new();
1176                for (name, bv) in [
1177                    ("a_lo", &a.lo),
1178                    ("a_hi", &a.hi),
1179                    ("b_lo", &b.lo),
1180                    ("b_hi", &b.hi),
1181                ] {
1182                    if let Some(v) = solver.value(bv) {
1183                        parts.push(format!("{name}={v}"));
1184                    }
1185                }
1186                let description = if parts.is_empty() {
1187                    "no model available".to_string()
1188                } else {
1189                    parts.join(", ")
1190                };
1191                Err(ValidationError::Counterexample {
1192                    wasm_op_label: label,
1193                    description,
1194                })
1195            }
1196            CheckOutcome::Unknown(_) => Err(ValidationError::SolverUnknown(label)),
1197        }
1198    }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203    use super::*;
1204    use crate::with_verification_context;
1205
1206    // ---- helpers ----------------------------------------------------------
1207
1208    /// Assert that `sel` certifies (Z3 returns `unsat` for the negation).
1209    fn assert_certifies(wasm: WasmOp, arm: Vec<ArmOp>) {
1210        let validator = Z3ArmValidator::new();
1211        let sel = CertifiedSelection::new(wasm.clone(), arm);
1212        match validator.validate(&sel) {
1213            Ok(w) => {
1214                assert_eq!(
1215                    w.solver_result,
1216                    SolverResultKind::Unsat,
1217                    "{wasm:?} did not certify"
1218                );
1219                assert_eq!(w.wasm_op_label, format!("{wasm:?}"));
1220            }
1221            other => panic!("expected {wasm:?} to certify, got {other:?}"),
1222        }
1223    }
1224
1225    /// i32 data-processing lowering: `OP R0, R0, R1`.
1226    fn dp_r0_r0_r1(make: fn(Reg, Reg, Operand2) -> ArmOp) -> Vec<ArmOp> {
1227        vec![make(Reg::R0, Reg::R0, Operand2::Reg(Reg::R1))]
1228    }
1229
1230    /// i32 comparison lowering: `CMP R0, R1; SetCond R0, cond`.
1231    fn i32_cmp(cond: Condition) -> Vec<ArmOp> {
1232        vec![
1233            ArmOp::Cmp {
1234                rn: Reg::R0,
1235                op2: Operand2::Reg(Reg::R1),
1236            },
1237            ArmOp::SetCond { rd: Reg::R0, cond },
1238        ]
1239    }
1240
1241    /// i64 comparison lowering: a single `I64SetCond` pseudo-op over the
1242    /// register pairs (R0:R1) and (R2:R3).
1243    fn i64_cmp(cond: Condition) -> Vec<ArmOp> {
1244        vec![ArmOp::I64SetCond {
1245            rd: Reg::R0,
1246            rn_lo: Reg::R0,
1247            rn_hi: Reg::R1,
1248            rm_lo: Reg::R2,
1249            rm_hi: Reg::R3,
1250            cond,
1251        }]
1252    }
1253
1254    /// i64 limb-wise logic lowering: `OP R0,R0,R2 ; OP R1,R1,R3`.
1255    fn i64_logic(make: fn(Reg, Reg, Operand2) -> ArmOp) -> Vec<ArmOp> {
1256        vec![
1257            make(Reg::R0, Reg::R0, Operand2::Reg(Reg::R2)),
1258            make(Reg::R1, Reg::R1, Operand2::Reg(Reg::R3)),
1259        ]
1260    }
1261
1262    // ---- existing prototype tests (kept passing) --------------------------
1263
1264    /// The headline test from the prototype: a correct selector picking `ADD`
1265    /// certifies; a wrong selector picking `SUB` is rejected.
1266    #[test]
1267    fn i32_add_certifies() {
1268        with_verification_context(|| {
1269            let validator = Z3ArmValidator::new();
1270
1271            let correct = CertifiedSelection::new(
1272                WasmOp::I32Add,
1273                vec![ArmOp::Add {
1274                    rd: Reg::R0,
1275                    rn: Reg::R0,
1276                    op2: Operand2::Reg(Reg::R1),
1277                }],
1278            );
1279            let witness = validator
1280                .validate(&correct)
1281                .expect("validator must accept I32Add → ADD");
1282            assert_eq!(witness.wasm_op_label, "I32Add");
1283            assert_eq!(witness.arm_op_count, 1);
1284            assert_eq!(witness.solver_result, SolverResultKind::Unsat);
1285
1286            let wrong = CertifiedSelection::new(
1287                WasmOp::I32Add,
1288                vec![ArmOp::Sub {
1289                    rd: Reg::R0,
1290                    rn: Reg::R0,
1291                    op2: Operand2::Reg(Reg::R1),
1292                }],
1293            );
1294            match validator.validate(&wrong) {
1295                Err(ValidationError::Counterexample { wasm_op_label, .. }) => {
1296                    assert_eq!(wasm_op_label, "I32Add");
1297                }
1298                other => panic!("expected Counterexample for SUB, got {other:?}"),
1299            }
1300        });
1301    }
1302
1303    /// Unsupported ops return a structured error, not a panic.
1304    #[test]
1305    fn unsupported_op_returns_structured_error() {
1306        with_verification_context(|| {
1307            let validator = Z3ArmValidator::new();
1308            // `Drop` is not in the i32/i64 arithmetic surface.
1309            let sel = CertifiedSelection::<WasmOp, ArmOp>::new(WasmOp::Drop, vec![]);
1310            match validator.validate(&sel) {
1311                Err(ValidationError::UnsupportedOp(op)) => assert_eq!(op, WasmOp::Drop),
1312                other => panic!("expected UnsupportedOp, got {other:?}"),
1313            }
1314        });
1315    }
1316
1317    /// CertifiedSelection plumbing: witnesses round-trip through
1318    /// `with_witness` / `is_certified` without losing information.
1319    #[test]
1320    fn certified_selection_witness_roundtrip() {
1321        let sel = CertifiedSelection::<WasmOp, ArmOp>::new(
1322            WasmOp::I32Add,
1323            vec![ArmOp::Add {
1324                rd: Reg::R0,
1325                rn: Reg::R0,
1326                op2: Operand2::Reg(Reg::R1),
1327            }],
1328        );
1329        assert!(!sel.is_certified());
1330        let witness = Witness {
1331            wasm_op_label: "I32Add".to_string(),
1332            arm_op_count: 1,
1333            solver_result: SolverResultKind::Unsat,
1334        };
1335        let certified = sel.with_witness(witness.clone());
1336        assert!(certified.is_certified());
1337        assert_eq!(certified.witness, Some(witness));
1338    }
1339
1340    // ---- i32 arithmetic / logic -------------------------------------------
1341
1342    #[test]
1343    fn i32_arith_logic_certifies() {
1344        with_verification_context(|| {
1345            assert_certifies(
1346                WasmOp::I32Add,
1347                dp_r0_r0_r1(|rd, rn, op2| ArmOp::Add { rd, rn, op2 }),
1348            );
1349            assert_certifies(
1350                WasmOp::I32Sub,
1351                dp_r0_r0_r1(|rd, rn, op2| ArmOp::Sub { rd, rn, op2 }),
1352            );
1353            assert_certifies(
1354                WasmOp::I32Mul,
1355                vec![ArmOp::Mul {
1356                    rd: Reg::R0,
1357                    rn: Reg::R0,
1358                    rm: Reg::R1,
1359                }],
1360            );
1361            assert_certifies(
1362                WasmOp::I32And,
1363                dp_r0_r0_r1(|rd, rn, op2| ArmOp::And { rd, rn, op2 }),
1364            );
1365            assert_certifies(
1366                WasmOp::I32Or,
1367                dp_r0_r0_r1(|rd, rn, op2| ArmOp::Orr { rd, rn, op2 }),
1368            );
1369            assert_certifies(
1370                WasmOp::I32Xor,
1371                dp_r0_r0_r1(|rd, rn, op2| ArmOp::Eor { rd, rn, op2 }),
1372            );
1373        });
1374    }
1375
1376    // ---- i32 shifts -------------------------------------------------------
1377
1378    #[test]
1379    fn i32_shifts_certify() {
1380        with_verification_context(|| {
1381            // WASM masks the shift count mod 32. The faithful lowering masks
1382            // R1 with #31 first, then does the register shift.
1383            let mask_then = |shift: ArmOp| {
1384                vec![
1385                    ArmOp::And {
1386                        rd: Reg::R1,
1387                        rn: Reg::R1,
1388                        op2: Operand2::Imm(31),
1389                    },
1390                    shift,
1391                ]
1392            };
1393            assert_certifies(
1394                WasmOp::I32Shl,
1395                mask_then(ArmOp::LslReg {
1396                    rd: Reg::R0,
1397                    rn: Reg::R0,
1398                    rm: Reg::R1,
1399                }),
1400            );
1401            assert_certifies(
1402                WasmOp::I32ShrU,
1403                mask_then(ArmOp::LsrReg {
1404                    rd: Reg::R0,
1405                    rn: Reg::R0,
1406                    rm: Reg::R1,
1407                }),
1408            );
1409            assert_certifies(
1410                WasmOp::I32ShrS,
1411                mask_then(ArmOp::AsrReg {
1412                    rd: Reg::R0,
1413                    rn: Reg::R0,
1414                    rm: Reg::R1,
1415                }),
1416            );
1417            // ARM ROR rotates mod 32 inherently, so it matches WASM rotr
1418            // directly without masking.
1419            assert_certifies(
1420                WasmOp::I32Rotr,
1421                vec![ArmOp::RorReg {
1422                    rd: Reg::R0,
1423                    rn: Reg::R0,
1424                    rm: Reg::R1,
1425                }],
1426            );
1427            // ROTL(x, s) = ROTR(x, -s): RSB R1, R1, #0 negates the amount,
1428            // then ROR rotates right by it.
1429            assert_certifies(
1430                WasmOp::I32Rotl,
1431                vec![
1432                    ArmOp::Rsb {
1433                        rd: Reg::R1,
1434                        rn: Reg::R1,
1435                        imm: 0,
1436                    },
1437                    ArmOp::RorReg {
1438                        rd: Reg::R0,
1439                        rn: Reg::R0,
1440                        rm: Reg::R1,
1441                    },
1442                ],
1443            );
1444        });
1445    }
1446
1447    // ---- i32 comparisons --------------------------------------------------
1448
1449    #[test]
1450    fn i32_comparisons_certify() {
1451        with_verification_context(|| {
1452            // Each comparison lowers to CMP + SetCond with the matching ARM
1453            // condition code. Signed: LT/LE/GT/GE; unsigned: LO/LS/HI/HS.
1454            assert_certifies(WasmOp::I32Eq, i32_cmp(Condition::EQ));
1455            assert_certifies(WasmOp::I32Ne, i32_cmp(Condition::NE));
1456            assert_certifies(WasmOp::I32LtS, i32_cmp(Condition::LT));
1457            assert_certifies(WasmOp::I32LeS, i32_cmp(Condition::LE));
1458            assert_certifies(WasmOp::I32GtS, i32_cmp(Condition::GT));
1459            assert_certifies(WasmOp::I32GeS, i32_cmp(Condition::GE));
1460            assert_certifies(WasmOp::I32LtU, i32_cmp(Condition::LO));
1461            assert_certifies(WasmOp::I32LeU, i32_cmp(Condition::LS));
1462            assert_certifies(WasmOp::I32GtU, i32_cmp(Condition::HI));
1463            assert_certifies(WasmOp::I32GeU, i32_cmp(Condition::HS));
1464        });
1465    }
1466
1467    #[test]
1468    fn i32_eqz_certifies() {
1469        with_verification_context(|| {
1470            // i32.eqz: (a == 0). CMP R0, #0 then SetCond EQ.
1471            assert_certifies(
1472                WasmOp::I32Eqz,
1473                vec![
1474                    ArmOp::Cmp {
1475                        rn: Reg::R0,
1476                        op2: Operand2::Imm(0),
1477                    },
1478                    ArmOp::SetCond {
1479                        rd: Reg::R0,
1480                        cond: Condition::EQ,
1481                    },
1482                ],
1483            );
1484        });
1485    }
1486
1487    // ---- i32 division -----------------------------------------------------
1488
1489    #[test]
1490    fn i32_div_certify() {
1491        with_verification_context(|| {
1492            // div_s / div_u map directly onto SDIV / UDIV (bvsdiv / bvudiv);
1493            // no symbolic multiply, so the equivalence is solver-tractable.
1494            // The non-trapping precondition excludes divisor 0 (and, for
1495            // div_s, INT_MIN/-1).
1496            assert_certifies(
1497                WasmOp::I32DivS,
1498                vec![ArmOp::Sdiv {
1499                    rd: Reg::R0,
1500                    rn: Reg::R0,
1501                    rm: Reg::R1,
1502                }],
1503            );
1504            assert_certifies(
1505                WasmOp::I32DivU,
1506                vec![ArmOp::Udiv {
1507                    rd: Reg::R0,
1508                    rn: Reg::R0,
1509                    rm: Reg::R1,
1510                }],
1511            );
1512        });
1513    }
1514
1515    /// `i32.rem_s` and `i32.rem_u` are scoped out — the `SDIV/UDIV` + `MLS`
1516    /// remainder identity `r = a - (a / b) * b` makes Z3 reason about a
1517    /// symbolic 32-bit multiply, which is SMT-intractable (see module docs).
1518    /// Confirm the validator reports them as `UnsupportedOp` rather than
1519    /// silently certifying or hanging.
1520    #[test]
1521    fn i32_rem_is_scoped_out() {
1522        with_verification_context(|| {
1523            let validator = Z3ArmValidator::new();
1524            for op in [WasmOp::I32RemS, WasmOp::I32RemU] {
1525                let sel = CertifiedSelection::<WasmOp, ArmOp>::new(op.clone(), vec![]);
1526                match validator.validate(&sel) {
1527                    Err(ValidationError::UnsupportedOp(got)) => assert_eq!(got, op),
1528                    other => panic!("expected {op:?} to be UnsupportedOp, got {other:?}"),
1529                }
1530            }
1531        });
1532    }
1533
1534    // ---- i64 arithmetic ---------------------------------------------------
1535
1536    #[test]
1537    fn i64_add_certifies() {
1538        with_verification_context(|| {
1539            // i64.add register-pair lowering:
1540            //   ADDS R0, R0, R2   ; lo = a_lo + b_lo, sets carry
1541            //   ADC  R1, R1, R3   ; hi = a_hi + b_hi + carry
1542            assert_certifies(
1543                WasmOp::I64Add,
1544                vec![
1545                    ArmOp::Adds {
1546                        rd: Reg::R0,
1547                        rn: Reg::R0,
1548                        op2: Operand2::Reg(Reg::R2),
1549                    },
1550                    ArmOp::Adc {
1551                        rd: Reg::R1,
1552                        rn: Reg::R1,
1553                        op2: Operand2::Reg(Reg::R3),
1554                    },
1555                ],
1556            );
1557        });
1558    }
1559
1560    #[test]
1561    fn i64_sub_certifies() {
1562        with_verification_context(|| {
1563            // i64.sub register-pair lowering:
1564            //   SUBS R0, R0, R2   ; lo = a_lo - b_lo, sets borrow (C)
1565            //   SBC  R1, R1, R3   ; hi = a_hi - b_hi - borrow
1566            assert_certifies(
1567                WasmOp::I64Sub,
1568                vec![
1569                    ArmOp::Subs {
1570                        rd: Reg::R0,
1571                        rn: Reg::R0,
1572                        op2: Operand2::Reg(Reg::R2),
1573                    },
1574                    ArmOp::Sbc {
1575                        rd: Reg::R1,
1576                        rn: Reg::R1,
1577                        op2: Operand2::Reg(Reg::R3),
1578                    },
1579                ],
1580            );
1581        });
1582    }
1583
1584    /// `i64.mul` certifies: the `I64Mul` register-pair pseudo-op lowering is
1585    /// proved equivalent to the WASM `i64.mul` reference for all 2^128 input
1586    /// pairs. Both the reference and the pseudo-op model the 64-bit product
1587    /// with Z3's native `bvmul`, so what this certifies is that the lowering
1588    /// reads the operand pairs and writes the destination pair correctly —
1589    /// a misrouted register (e.g. swapping a limb) is caught. The expansion
1590    /// of `I64Mul` to actual UMULL + MLA instructions happens below the
1591    /// `ArmOp` level the validator inspects.
1592    #[test]
1593    fn i64_mul_certifies() {
1594        with_verification_context(|| {
1595            assert_certifies(
1596                WasmOp::I64Mul,
1597                vec![ArmOp::I64Mul {
1598                    rd_lo: Reg::R0,
1599                    rd_hi: Reg::R1,
1600                    rn_lo: Reg::R0,
1601                    rn_hi: Reg::R1,
1602                    rm_lo: Reg::R2,
1603                    rm_hi: Reg::R3,
1604                }],
1605            );
1606        });
1607    }
1608
1609    #[test]
1610    fn i64_logic_certifies() {
1611        with_verification_context(|| {
1612            assert_certifies(
1613                WasmOp::I64And,
1614                i64_logic(|rd, rn, op2| ArmOp::And { rd, rn, op2 }),
1615            );
1616            assert_certifies(
1617                WasmOp::I64Or,
1618                i64_logic(|rd, rn, op2| ArmOp::Orr { rd, rn, op2 }),
1619            );
1620            assert_certifies(
1621                WasmOp::I64Xor,
1622                i64_logic(|rd, rn, op2| ArmOp::Eor { rd, rn, op2 }),
1623            );
1624        });
1625    }
1626
1627    // ---- i64 shifts / rotates ---------------------------------------------
1628    //
1629    // i64 shifts lower to multi-instruction sequences with a runtime branch
1630    // on whether the (mod-64) amount is < 32 or >= 32. The validator's
1631    // `i64_shl` / `i64_shr_u` / `i64_shr_s` references model exactly that
1632    // case split. Each shift/rotate is validated in two layers, mirroring
1633    // `i64_mul_certifies`: (1) the limb reference is proved equal to Z3's
1634    // native 64-bit shift for all inputs; (2) the composite pseudo-op
1635    // lowering is certified to wire the registers correctly.
1636
1637    /// Prove a limb-shift reference equals the native 64-bit shift for all
1638    /// inputs and all (mod-64) amounts. `native` builds the 64-bit truth.
1639    fn prove_shift_reference(
1640        limb_fn: fn(&I64Pair, &BV) -> I64Pair,
1641        native: fn(&BV, &BV) -> BV,
1642        label: &str,
1643    ) {
1644        let mut solver = new_solver();
1645        let a = I64Pair {
1646            lo: sym32("sa_lo"),
1647            hi: sym32("sa_hi"),
1648        };
1649        // Symbolic amount, already reduced mod 64 (the masked count).
1650        let amt = sym32("samt");
1651        solver.assert(&amt.bvult(k32(64)));
1652
1653        let limb = limb_fn(&a, &amt);
1654        let limb64 = limb.hi.concat(&limb.lo);
1655        // 64-bit ground truth. The shift amount must be a 64-bit BV for the
1656        // native op; zero-extend the 32-bit amount into the high half.
1657        let a64 = a.hi.concat(&a.lo);
1658        let amt64 = k32(0).concat(&amt);
1659        let truth = native(&a64, &amt64);
1660
1661        solver.assert(&limb64.eq(&truth).not());
1662        assert_eq!(
1663            solver.check(),
1664            CheckOutcome::Unsat,
1665            "{label} limb reference must equal the native 64-bit shift for all inputs"
1666        );
1667    }
1668
1669    #[test]
1670    fn i64_shift_references_match_native() {
1671        with_verification_context(|| {
1672            prove_shift_reference(i64_shl, |x, s| x.bvshl(s), "i64.shl");
1673            prove_shift_reference(i64_shr_u, |x, s| x.bvlshr(s), "i64.shr_u");
1674            prove_shift_reference(i64_shr_s, |x, s| x.bvashr(s), "i64.shr_s");
1675        });
1676    }
1677
1678    #[test]
1679    fn i64_shifts_certify() {
1680        with_verification_context(|| {
1681            // Composite pseudo-op lowering: shift the (R0:R1) pair by R2.
1682            assert_certifies(
1683                WasmOp::I64Shl,
1684                vec![ArmOp::I64Shl {
1685                    rd_lo: Reg::R0,
1686                    rd_hi: Reg::R1,
1687                    rn_lo: Reg::R0,
1688                    rn_hi: Reg::R1,
1689                    rm_lo: Reg::R2,
1690                    rm_hi: Reg::R3,
1691                }],
1692            );
1693            assert_certifies(
1694                WasmOp::I64ShrU,
1695                vec![ArmOp::I64ShrU {
1696                    rd_lo: Reg::R0,
1697                    rd_hi: Reg::R1,
1698                    rn_lo: Reg::R0,
1699                    rn_hi: Reg::R1,
1700                    rm_lo: Reg::R2,
1701                    rm_hi: Reg::R3,
1702                }],
1703            );
1704            assert_certifies(
1705                WasmOp::I64ShrS,
1706                vec![ArmOp::I64ShrS {
1707                    rd_lo: Reg::R0,
1708                    rd_hi: Reg::R1,
1709                    rn_lo: Reg::R0,
1710                    rn_hi: Reg::R1,
1711                    rm_lo: Reg::R2,
1712                    rm_hi: Reg::R3,
1713                }],
1714            );
1715        });
1716    }
1717
1718    #[test]
1719    fn i64_rotates_certify() {
1720        with_verification_context(|| {
1721            assert_certifies(
1722                WasmOp::I64Rotl,
1723                vec![ArmOp::I64Rotl {
1724                    rdlo: Reg::R0,
1725                    rdhi: Reg::R1,
1726                    rnlo: Reg::R0,
1727                    rnhi: Reg::R1,
1728                    shift: Reg::R2,
1729                }],
1730            );
1731            assert_certifies(
1732                WasmOp::I64Rotr,
1733                vec![ArmOp::I64Rotr {
1734                    rdlo: Reg::R0,
1735                    rdhi: Reg::R1,
1736                    rnlo: Reg::R0,
1737                    rnhi: Reg::R1,
1738                    shift: Reg::R2,
1739                }],
1740            );
1741        });
1742    }
1743
1744    // ---- i64 comparisons --------------------------------------------------
1745
1746    /// Prove the lexicographic `i64_lt_u` / `i64_lt_s` references equal Z3's
1747    /// native 64-bit comparisons for all inputs. Both `wasm_reference` and
1748    /// the `I64SetCond` lowering model are built on these helpers, so this
1749    /// closes the loop: it shows the *helper itself* is the right relation.
1750    #[test]
1751    fn i64_compare_references_match_native() {
1752        with_verification_context(|| {
1753            let mut solver = new_solver();
1754            let a = I64Pair {
1755                lo: sym32("ca_lo"),
1756                hi: sym32("ca_hi"),
1757            };
1758            let b = I64Pair {
1759                lo: sym32("cb_lo"),
1760                hi: sym32("cb_hi"),
1761            };
1762            let a64 = a.hi.concat(&a.lo);
1763            let b64 = b.hi.concat(&b.lo);
1764
1765            // i64_lt_u must equal native unsigned 64-bit less-than.
1766            let lt_u_wrong = i64_lt_u(&a, &b).eq(a64.bvult(&b64)).not();
1767            // i64_lt_s must equal native signed 64-bit less-than.
1768            let lt_s_wrong = i64_lt_s(&a, &b).eq(a64.bvslt(&b64)).not();
1769            solver.assert(&Bool::or(&[&lt_u_wrong, &lt_s_wrong]));
1770            assert_eq!(
1771                solver.check(),
1772                CheckOutcome::Unsat,
1773                "i64 lexicographic compare references must match native 64-bit comparisons"
1774            );
1775        });
1776    }
1777
1778    #[test]
1779    fn i64_comparisons_certify() {
1780        with_verification_context(|| {
1781            // Each i64 comparison lowers to a single I64SetCond pseudo-op
1782            // over the register pairs (R0:R1) and (R2:R3).
1783            assert_certifies(WasmOp::I64Eq, i64_cmp(Condition::EQ));
1784            assert_certifies(WasmOp::I64Ne, i64_cmp(Condition::NE));
1785            assert_certifies(WasmOp::I64LtS, i64_cmp(Condition::LT));
1786            assert_certifies(WasmOp::I64LeS, i64_cmp(Condition::LE));
1787            assert_certifies(WasmOp::I64GtS, i64_cmp(Condition::GT));
1788            assert_certifies(WasmOp::I64GeS, i64_cmp(Condition::GE));
1789            assert_certifies(WasmOp::I64LtU, i64_cmp(Condition::LO));
1790            assert_certifies(WasmOp::I64LeU, i64_cmp(Condition::LS));
1791            assert_certifies(WasmOp::I64GtU, i64_cmp(Condition::HI));
1792            assert_certifies(WasmOp::I64GeU, i64_cmp(Condition::HS));
1793        });
1794    }
1795
1796    #[test]
1797    fn i64_eqz_certifies() {
1798        with_verification_context(|| {
1799            // i64.eqz: 1 iff the whole register pair is zero.
1800            assert_certifies(
1801                WasmOp::I64Eqz,
1802                vec![ArmOp::I64SetCondZ {
1803                    rd: Reg::R0,
1804                    rn_lo: Reg::R0,
1805                    rn_hi: Reg::R1,
1806                }],
1807            );
1808        });
1809    }
1810
1811    // ---- negative tests: wrong lowerings must be rejected -----------------
1812
1813    /// A deliberately wrong `i64.add` lowering: the high limb uses plain `ADD`
1814    /// instead of `ADC`, so it drops the carry from the low limb. The
1815    /// validator must find a counterexample (any input whose low limbs
1816    /// produce a carry, e.g. `a_lo = b_lo = 0x8000_0000`).
1817    #[test]
1818    fn wrong_i64_add_lowering_rejected() {
1819        with_verification_context(|| {
1820            let validator = Z3ArmValidator::new();
1821            let wrong = CertifiedSelection::new(
1822                WasmOp::I64Add,
1823                vec![
1824                    ArmOp::Adds {
1825                        rd: Reg::R0,
1826                        rn: Reg::R0,
1827                        op2: Operand2::Reg(Reg::R2),
1828                    },
1829                    // BUG: should be ADC — this plain ADD loses the carry.
1830                    ArmOp::Add {
1831                        rd: Reg::R1,
1832                        rn: Reg::R1,
1833                        op2: Operand2::Reg(Reg::R3),
1834                    },
1835                ],
1836            );
1837            match validator.validate(&wrong) {
1838                Err(ValidationError::Counterexample { wasm_op_label, .. }) => {
1839                    assert_eq!(wasm_op_label, "I64Add");
1840                }
1841                other => {
1842                    panic!("expected Counterexample for carry-dropping i64.add, got {other:?}")
1843                }
1844            }
1845        });
1846    }
1847
1848    /// A deliberately wrong i32 comparison lowering: `i32.lt_s` lowered with
1849    /// the *unsigned* condition `LO`. Signed and unsigned less-than disagree
1850    /// whenever exactly one operand is negative (e.g. `a = -1, b = 1`), so the
1851    /// validator must reject it.
1852    #[test]
1853    fn wrong_i32_lt_s_lowering_rejected() {
1854        with_verification_context(|| {
1855            let validator = Z3ArmValidator::new();
1856            let wrong = CertifiedSelection::new(
1857                WasmOp::I32LtS,
1858                vec![
1859                    ArmOp::Cmp {
1860                        rn: Reg::R0,
1861                        op2: Operand2::Reg(Reg::R1),
1862                    },
1863                    // BUG: signed lt_s must use LT, not the unsigned LO.
1864                    ArmOp::SetCond {
1865                        rd: Reg::R0,
1866                        cond: Condition::LO,
1867                    },
1868                ],
1869            );
1870            match validator.validate(&wrong) {
1871                Err(ValidationError::Counterexample { wasm_op_label, .. }) => {
1872                    assert_eq!(wasm_op_label, "I32LtS");
1873                }
1874                other => panic!("expected Counterexample for lt_s-as-LO, got {other:?}"),
1875            }
1876        });
1877    }
1878
1879    /// A deliberately wrong `i64.shl` would be caught too: feeding the
1880    /// `i64.shl` op an ARM sequence that only shifts the low limb leaves the
1881    /// high limb untouched, which the reference rejects for any nonzero
1882    /// shift. We exercise it as a selection whose ARM sequence is a single
1883    /// low-limb shift.
1884    #[test]
1885    fn wrong_i64_shl_lowering_rejected() {
1886        with_verification_context(|| {
1887            let validator = Z3ArmValidator::new();
1888            let wrong = CertifiedSelection::new(
1889                WasmOp::I64Shl,
1890                vec![
1891                    // Only shifts the low limb by R2; high limb R1 is left
1892                    // as the input a_hi — wrong for any shift amount > 0.
1893                    ArmOp::LslReg {
1894                        rd: Reg::R0,
1895                        rn: Reg::R0,
1896                        rm: Reg::R2,
1897                    },
1898                ],
1899            );
1900            match validator.validate(&wrong) {
1901                Err(ValidationError::Counterexample { wasm_op_label, .. }) => {
1902                    assert_eq!(wasm_op_label, "I64Shl");
1903                }
1904                other => panic!("expected Counterexample for partial i64.shl, got {other:?}"),
1905            }
1906        });
1907    }
1908
1909    /// i64 division is scoped out — confirm it reports `UnsupportedOp`
1910    /// rather than silently certifying.
1911    #[test]
1912    fn i64_div_is_scoped_out() {
1913        with_verification_context(|| {
1914            let validator = Z3ArmValidator::new();
1915            for op in [
1916                WasmOp::I64DivS,
1917                WasmOp::I64DivU,
1918                WasmOp::I64RemS,
1919                WasmOp::I64RemU,
1920            ] {
1921                let sel = CertifiedSelection::<WasmOp, ArmOp>::new(op.clone(), vec![]);
1922                match validator.validate(&sel) {
1923                    Err(ValidationError::UnsupportedOp(got)) => assert_eq!(got, op),
1924                    other => panic!("expected {op:?} to be UnsupportedOp, got {other:?}"),
1925                }
1926            }
1927        });
1928    }
1929}