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