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(®s, op2)?);
810 }
811 ArmOp::Sub { rd, rn, op2 } => {
812 regs[idx(rd)] = regs[idx(rn)].bvsub(&eval_op2(®s, 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(®s, 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(®s, 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(®s, 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(®s, op2)?).bvsub(&borrow);
839 }
840 ArmOp::Mul { rd, rn, rm } => {
841 regs[idx(rd)] = regs[idx(rn)].bvmul(®s[idx(rm)]);
842 }
843 ArmOp::And { rd, rn, op2 } => {
844 regs[idx(rd)] = regs[idx(rn)].bvand(&eval_op2(®s, op2)?);
845 }
846 ArmOp::Orr { rd, rn, op2 } => {
847 regs[idx(rd)] = regs[idx(rn)].bvor(&eval_op2(®s, op2)?);
848 }
849 ArmOp::Eor { rd, rn, op2 } => {
850 regs[idx(rd)] = regs[idx(rn)].bvxor(&eval_op2(®s, op2)?);
851 }
852 ArmOp::Mov { rd, op2 } => {
853 regs[idx(rd)] = eval_op2(®s, op2)?;
854 }
855 ArmOp::Mvn { rd, op2 } => {
856 regs[idx(rd)] = eval_op2(®s, 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(®s[idx(rm)]);
864 }
865 ArmOp::LsrReg { rd, rn, rm } => {
866 regs[idx(rd)] = regs[idx(rn)].bvlshr(®s[idx(rm)]);
867 }
868 ArmOp::AsrReg { rd, rn, rm } => {
869 regs[idx(rd)] = regs[idx(rn)].bvashr(®s[idx(rm)]);
870 }
871 ArmOp::RorReg { rd, rn, rm } => {
872 regs[idx(rd)] = regs[idx(rn)].bvrotr(®s[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(®s[idx(rn)]);
890 }
891 ArmOp::Sdiv { rd, rn, rm } => {
892 regs[idx(rd)] = regs[idx(rn)].bvsdiv(®s[idx(rm)]);
893 }
894 ArmOp::Udiv { rd, rn, rm } => {
895 regs[idx(rd)] = regs[idx(rn)].bvudiv(®s[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(®s[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(®s, 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(&[®s[idx(rn_lo)].eq(k32(0)), ®s[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(&[<_u_wrong, <_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}