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(®s, op2)?);
825 }
826 ArmOp::Sub { rd, rn, op2 } => {
827 regs[idx(rd)] = regs[idx(rn)].bvsub(&eval_op2(®s, 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(®s, 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(®s, 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(®s, 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(®s, op2)?).bvsub(&borrow);
854 }
855 ArmOp::Mul { rd, rn, rm } => {
856 regs[idx(rd)] = regs[idx(rn)].bvmul(®s[idx(rm)]);
857 }
858 ArmOp::And { rd, rn, op2 } => {
859 regs[idx(rd)] = regs[idx(rn)].bvand(&eval_op2(®s, op2)?);
860 }
861 ArmOp::Orr { rd, rn, op2 } => {
862 regs[idx(rd)] = regs[idx(rn)].bvor(&eval_op2(®s, op2)?);
863 }
864 ArmOp::Eor { rd, rn, op2 } => {
865 regs[idx(rd)] = regs[idx(rn)].bvxor(&eval_op2(®s, op2)?);
866 }
867 ArmOp::Mov { rd, op2 } => {
868 regs[idx(rd)] = eval_op2(®s, op2)?;
869 }
870 ArmOp::Mvn { rd, op2 } => {
871 regs[idx(rd)] = eval_op2(®s, 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(®s[idx(rm)]));
889 }
890 ArmOp::LsrReg { rd, rn, rm } => {
891 regs[idx(rd)] = regs[idx(rn)].bvlshr(shift_amount_rm_7_0(®s[idx(rm)]));
892 }
893 ArmOp::AsrReg { rd, rn, rm } => {
894 regs[idx(rd)] = regs[idx(rn)].bvashr(shift_amount_rm_7_0(®s[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(®s[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(®s[idx(rn)]);
919 }
920 ArmOp::Sdiv { rd, rn, rm } => {
921 regs[idx(rd)] = regs[idx(rn)].bvsdiv(®s[idx(rm)]);
922 }
923 ArmOp::Udiv { rd, rn, rm } => {
924 regs[idx(rd)] = regs[idx(rn)].bvudiv(®s[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(®s[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(®s, 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(&[®s[idx(rn_lo)].eq(k32(0)), ®s[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(&[<_u_wrong, <_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}