Skip to main content

synth_verify/
translation_validator.rs

1//! Translation Validator - Proves equivalence between WASM and ARM code
2//!
3//! This module implements SMT-based translation validation inspired by Alive2.
4//! For each synthesis rule WASM → ARM, we prove that the ARM code has
5//! semantically equivalent behavior to the WASM code.
6//!
7//! # Verification Approach
8//!
9//! 1. Create symbolic inputs for both WASM and ARM
10//! 2. Encode WASM semantics as SMT formula phi_wasm
11//! 3. Encode ARM semantics as SMT formula phi_arm
12//! 4. Assert: phi_wasm(inputs) == phi_arm(inputs)
13//! 5. Check satisfiability - if UNSAT, then equivalence is proven
14//!
15//! # Example
16//!
17//! For the rule: WASM `i32.add` -> ARM `ADD Rd, Rn, Rm`
18//!
19//! We prove: forall a,b. i32.add(a, b) == ADD(a, b)
20
21use crate::arm_semantics::{ArmSemantics, ArmState};
22use crate::solver::{CheckOutcome, new_solver};
23use crate::term::{BV, Bool};
24use crate::wasm_semantics::WasmSemantics;
25use synth_core::WasmOp;
26use synth_synthesis::{ArmOp, Reg, SynthesisRule};
27use thiserror::Error;
28
29/// Verification error types
30#[derive(Debug, Error)]
31pub enum VerificationError {
32    #[error("Translation is incorrect: counterexample found")]
33    CounterexampleFound {
34        wasm_result: String,
35        arm_result: String,
36        inputs: Vec<String>,
37    },
38
39    #[error("Verification timeout after {0}ms")]
40    Timeout(u64),
41
42    #[error("Unsupported operation: {0}")]
43    UnsupportedOperation(String),
44
45    #[error("SMT solver error: {0}")]
46    SolverError(String),
47
48    #[error("Invalid synthesis rule: {0}")]
49    InvalidRule(String),
50}
51
52/// Result of translation validation
53#[derive(Debug, Clone, PartialEq)]
54pub enum ValidationResult {
55    /// Translation is provably correct
56    Verified,
57
58    /// Counterexample found - translation is incorrect
59    Invalid { counterexample: Vec<(String, i64)> },
60
61    /// Verification inconclusive (timeout or unsupported operations)
62    Unknown { reason: String },
63}
64
65/// Module facts that define the WASM-side `call_indirect` trap condition
66/// (VCR-VER-002, #166): what the SPEC demands for the dispatched table, to be
67/// checked against the guards the selector actually resolved into the
68/// `ArmOp::CallIndirect` pseudo-op.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct CallIndirectSpec {
71    /// The dispatched table's element count (compile-time, per #642).
72    pub table_size: u32,
73    /// Whether the table may contain uninitialized (null) slots — if so, the
74    /// lowering MUST carry the #664 null check.
75    pub may_have_null_slot: bool,
76    /// `Some(expected_type_id)` when the table is heterogeneous and WASM's
77    /// §4.4.8 type check must be discharged at RUNTIME (#676); `None` when
78    /// the closed-world verdict discharges it at compile time.
79    pub heterogeneous_expected_type: Option<u32>,
80}
81
82/// Translation validator over the configured SMT engine (see
83/// [`crate::solver::new_solver`]: ordeal by default, optionally
84/// cross-checked against Z3 when `SYNTH_SOLVER_DIFF=1`).
85pub struct TranslationValidator {
86    wasm_encoder: WasmSemantics,
87    arm_encoder: ArmSemantics,
88    timeout_ms: u64,
89}
90
91impl Default for TranslationValidator {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl TranslationValidator {
98    /// Create a new translation validator
99    pub fn new() -> Self {
100        Self {
101            wasm_encoder: WasmSemantics::new(),
102            arm_encoder: ArmSemantics::new(),
103            timeout_ms: 30000, // 30 seconds default
104        }
105    }
106
107    /// Set verification timeout in milliseconds
108    pub fn set_timeout(&mut self, timeout_ms: u64) {
109        self.timeout_ms = timeout_ms;
110    }
111
112    /// Verify a synthesis rule
113    ///
114    /// Proves that the ARM code generated by the rule has equivalent semantics
115    /// to the WASM code matched by the pattern.
116    pub fn verify_rule(&self, rule: &SynthesisRule) -> Result<ValidationResult, VerificationError> {
117        // Extract WASM operation from pattern
118        let wasm_op = match &rule.pattern {
119            synth_synthesis::Pattern::WasmInstr(op) => op,
120            _ => {
121                return Err(VerificationError::UnsupportedOperation(
122                    "Only single WASM instruction patterns are supported".to_string(),
123                ));
124            }
125        };
126
127        // Extract ARM operations from replacement
128        let arm_ops = match &rule.replacement {
129            synth_synthesis::Replacement::ArmInstr(op) => vec![op.clone()],
130            synth_synthesis::Replacement::ArmSequence(ops) => ops.clone(),
131            _ => {
132                return Err(VerificationError::UnsupportedOperation(
133                    "Only ARM instruction replacements are supported".to_string(),
134                ));
135            }
136        };
137
138        // VCR-VER-002 (#166): PARTIAL ops (they trap on some inputs) MUST go
139        // through the trap-preservation VC — a value-only proof over a total
140        // model cannot see a dropped trap, and for div/rem the value-only
141        // query is not even well-posed (SMT bvsdiv is total where WASM
142        // traps). The trap VC subsumes the value obligation where the value
143        // is modeled (div/rem: trap clause AND guarded value clause).
144        if Self::is_trap_gated_op(wasm_op) {
145            return self.verify_trap_preservation(wasm_op, &arm_ops);
146        }
147
148        self.verify_equivalence(wasm_op, &arm_ops)
149    }
150
151    /// The partial-op surface whose lowerings are MANDATORILY routed through
152    /// the trap-preservation VC by [`Self::verify_rule`].
153    fn is_trap_gated_op(wasm_op: &WasmOp) -> bool {
154        matches!(
155            wasm_op,
156            WasmOp::I32DivS
157                | WasmOp::I32DivU
158                | WasmOp::I32RemS
159                | WasmOp::I32RemU
160                // i64 div/rem (#756): field-derived trap-condition VC (the
161                // pseudo-op carries the guard as an `elide_*` boolean; the
162                // 64-bit quotient is not modeled, so no value clause).
163                | WasmOp::I64DivS
164                | WasmOp::I64DivU
165                | WasmOp::I64RemS
166                | WasmOp::I64RemU
167                | WasmOp::Unreachable
168                | WasmOp::I32Load { .. }
169                | WasmOp::I32Load8S { .. }
170                | WasmOp::I32Load8U { .. }
171                | WasmOp::I32Load16S { .. }
172                | WasmOp::I32Load16U { .. }
173                | WasmOp::I32Store { .. }
174                | WasmOp::I32Store8 { .. }
175                | WasmOp::I32Store16 { .. }
176                | WasmOp::I32TruncF32S
177                | WasmOp::I32TruncF32U
178                // f64→i32 trunc (#756): D-register domain guard, condition-only
179                // VC. NOT `I64TruncF64S/U` / `I64TruncF32S/U`: since #869 the
180                // shipping `select_with_stack` DOES lower them (i64 domain
181                // guard + #782 decompose), but they are never expressed as a
182                // `SynthesisRule` (this gate's input), and the symbolic ARM
183                // exec model does not yet interpret the F64 compare/decompose
184                // chain their guard rides on — a gate entry here would
185                // false-red, not protect. Their trap behavior is
186                // EXECUTION-gated instead (the #869 differential runs every
187                // NaN/±inf/2^63/-2^63/2^64 trap row on both sides); the
188                // symbolic wiring is the named #756-class residual.
189                | WasmOp::I32TruncF64S
190                | WasmOp::I32TruncF64U
191        )
192    }
193
194    /// Verify equivalence between a WASM operation and ARM operations
195    pub fn verify_equivalence(
196        &self,
197        wasm_op: &WasmOp,
198        arm_ops: &[ArmOp],
199    ) -> Result<ValidationResult, VerificationError> {
200        self.verify_equivalence_parameterized(wasm_op, arm_ops, &[])
201    }
202
203    /// Verify equivalence with concrete parameter values
204    pub fn verify_equivalence_parameterized(
205        &self,
206        wasm_op: &WasmOp,
207        arm_ops: &[ArmOp],
208        concrete_params: &[(usize, i64)],
209    ) -> Result<ValidationResult, VerificationError> {
210        let mut solver = new_solver();
211
212        // Create inputs - some symbolic, some concrete
213        let num_inputs = self.get_num_inputs(wasm_op);
214        let mut inputs: Vec<BV> = Vec::new();
215
216        for i in 0..num_inputs {
217            let input = if let Some((_, value)) = concrete_params.iter().find(|(idx, _)| *idx == i)
218            {
219                // Concrete value
220                BV::from_i64(*value, 32)
221            } else {
222                // Symbolic value
223                BV::new_const(format!("input_{}", i), 32)
224            };
225            inputs.push(input);
226        }
227
228        // Encode WASM semantics
229        let wasm_result = self.wasm_encoder.encode_op(wasm_op, &inputs);
230
231        // Encode ARM semantics
232        let arm_result = self.encode_arm_sequence(arm_ops, &inputs)?;
233
234        // Assert that results are NOT equal
235        // If this is UNSAT, then the results are always equal (proven correct)
236        // If this is SAT, we found a counterexample
237        solver.assert(&wasm_result.eq(&arm_result).not());
238
239        match solver.check() {
240            CheckOutcome::Unsat => {
241                // Proven correct - no inputs exist where results differ
242                Ok(ValidationResult::Verified)
243            }
244
245            CheckOutcome::Sat => {
246                // Found counterexample: read the differing inputs back from
247                // the model (symbolic inputs only — concrete params have no
248                // model entry). Values are reported unsigned, as before.
249                let mut counterexample = Vec::new();
250                for (i, input) in inputs.iter().enumerate() {
251                    if let Some(value) = solver.value(input)
252                        && let Ok(int_val) = i64::try_from(value)
253                    {
254                        counterexample.push((format!("input_{}", i), int_val));
255                    }
256                }
257
258                Ok(ValidationResult::Invalid { counterexample })
259            }
260
261            CheckOutcome::Unknown(reason) => {
262                // Verification inconclusive
263                Ok(ValidationResult::Unknown {
264                    reason: format!("SMT solver returned unknown: {reason}"),
265                })
266            }
267        }
268    }
269
270    /// Encode a sequence of ARM operations
271    fn encode_arm_sequence(
272        &self,
273        arm_ops: &[ArmOp],
274        inputs: &[BV],
275    ) -> Result<BV, VerificationError> {
276        let mut state = ArmState::new_symbolic();
277
278        // Initialize input registers
279        for (i, input) in inputs.iter().enumerate() {
280            let reg = match i {
281                0 => Reg::R0,
282                1 => Reg::R1,
283                2 => Reg::R2,
284                _ => {
285                    return Err(VerificationError::UnsupportedOperation(format!(
286                        "Too many inputs: {}",
287                        inputs.len()
288                    )));
289                }
290            };
291            state.set_reg(&reg, input.clone());
292        }
293
294        // Execute ARM operations.
295        //
296        // #923: an op `ArmSemantics` has no model for is a LOUD decline here,
297        // never a silent skip. `encode_op`'s default arm used to leave the
298        // state untouched, which made an unmodeled instruction invisible to
299        // this VC — and "invisible" is the false-ACCEPT direction, not the
300        // conservative one: the sequence `ADD r0,r0,r1 ; UXTB r0,r0` returns
301        // `(x+y) & 0xFF` on silicon, but with `UXTB` skipped the model saw a
302        // plain `x+y` and returned `Verified` for `i32.add` (measured on
303        // v0.57 `main`). A VC whose ARM side executed only SOME of the
304        // sequence proves nothing about the sequence.
305        for arm_op in arm_ops {
306            self.arm_encoder.encode_op(arm_op, &mut state);
307            if let Some(name) = &state.unmodeled {
308                return Err(VerificationError::UnsupportedOperation(format!(
309                    "ARM op `{name}` has no semantics in ArmSemantics::encode_op — \
310                     declining rather than verifying a partially-executed sequence (#923)"
311                )));
312            }
313        }
314
315        // Extract result from R0 (ARM calling convention)
316        Ok(self.arm_encoder.extract_result(&state, &Reg::R0))
317    }
318
319    /// Verify operation for all parameter values in a range
320    pub fn verify_parameterized_range<F>(
321        &self,
322        wasm_op: &WasmOp,
323        create_arm_ops: F,
324        param_index: usize,
325        range: std::ops::Range<i64>,
326    ) -> Result<ValidationResult, VerificationError>
327    where
328        F: Fn(i64) -> Vec<ArmOp>,
329    {
330        for value in range {
331            let arm_ops = create_arm_ops(value);
332            let result =
333                self.verify_equivalence_parameterized(wasm_op, &arm_ops, &[(param_index, value)])?;
334
335            match result {
336                ValidationResult::Verified => continue,
337                ValidationResult::Invalid { counterexample } => {
338                    return Ok(ValidationResult::Invalid {
339                        counterexample: counterexample
340                            .into_iter()
341                            .map(|(k, v)| (format!("{} (param={})", k, value), v))
342                            .collect(),
343                    });
344                }
345                ValidationResult::Unknown { reason } => {
346                    return Ok(ValidationResult::Unknown {
347                        reason: format!("Failed at param={}: {}", value, reason),
348                    });
349                }
350            }
351        }
352
353        Ok(ValidationResult::Verified)
354    }
355
356    /// VCR-VER-002 (#166): mandatory **trap-preservation** obligation for the
357    /// partial-op lowerings whose ARM trap condition synth can DERIVE from the
358    /// emitted sequence. The ARM side is no longer a structural `Udf`-presence
359    /// proxy: [`ArmSemantics::encode_sequence_br`] threads a `may_trap` term
360    /// through the exec model — guard branches condition it, `UDF` execution
361    /// accumulates it — so a dropped, inverted, or wrong-register guard
362    /// derives a trap condition that fails the VC.
363    ///
364    /// # Covered classes (LIVE, derived ARM trap term)
365    ///
366    /// | class | VC | operand convention |
367    /// |---|---|---|
368    /// | i32 div/rem | full ([`crate::trap::prove_trap_equivalence`]: trap AND guarded value) | dividend R0, divisor R1, result R0 |
369    /// | i64 div/rem | trap-condition only (field-derived; 64-bit quotient not modeled) | divisor R2:R3 (`rmlo`/`rmhi`), guards from the pseudo-op's `elide_*` fields |
370    /// | `unreachable` | trap-condition only | none |
371    /// | i32 load/store (all widths) | trap-condition only (no memory-contents model) | address R0 (+ store value R1), linear-memory size R10 |
372    /// | `i32.trunc_f32_s/u` | trap-condition only (no float→int value model) | operand S0, result R0 |
373    /// | `i32.trunc_f64_s/u` | trap-condition only (no float→int value model) | operand D0, bound scratch D1, result R0 |
374    ///
375    /// `call_indirect` goes through
376    /// [`Self::verify_call_indirect_trap_preservation`] (it needs module
377    /// facts as the spec side).
378    ///
379    /// # i64 div/rem trust boundary (like `call_indirect`)
380    ///
381    /// ARM32 has no 64-bit divide instruction: the shipped lowering emits an
382    /// `ArmOp::I64Div{S,U}`/`I64Rem{S,U}` PSEUDO-OP whose result registers the
383    /// exec-model leaves symbolic (no `bvsdiv` term) and whose trap guards are
384    /// carried as `elide_zero_guard`/`elide_overflow_guard` boolean FIELDS.
385    /// The gate certifies the SELECTOR's guard-elision decision: the ARM trap
386    /// term is CONSTRUCTED from those fields (a set `elide_*` deletes the
387    /// corresponding clause), so a lowering that elides a guard whose fact was
388    /// not discharged is reported `Invalid`. The encoder's expansion of the
389    /// pseudo-op into `ORRS`/`BNE`/`UDF`/library-call bytes is separately
390    /// execution-gated (the i64 div/rem CI oracles).
391    ///
392    /// # Held out, honestly
393    ///
394    /// - **`i64.trunc_f64_s/u`** — the selector loud-declines it (i64 register
395    ///   pairs on 32-bit ARM are unsupported), so there is NO shipped lowering
396    ///   to derive an ARM trap term from. The trap CLASSIFIER for these ops
397    ///   (`trap_trunc(F64, I64, …)`, incl. the item-4 2^63/-2^63/2^64
398    ///   boundaries) is unit-gated in `tests/trap_preservation.rs`.
399    pub fn verify_trap_preservation(
400        &self,
401        wasm_op: &WasmOp,
402        arm_ops: &[ArmOp],
403    ) -> Result<ValidationResult, VerificationError> {
404        match wasm_op {
405            WasmOp::I32DivS | WasmOp::I32DivU | WasmOp::I32RemS | WasmOp::I32RemU => {
406                self.verify_div_rem_trap_preservation(wasm_op, arm_ops)
407            }
408            // i64 rem_u/rem_s carry a REAL value model now (native
409            // `BvTerm::Urem`/`bvsrem` — VCR-VER, #825/#836): the VC asserts
410            // both the ÷0 trap AND the 64-bit remainder value, so a lowering
411            // that computes the wrong remainder — or writes it to the wrong
412            // register pair — is rejected. i64 div still leaves its 64-bit
413            // quotient symbolic (the full quotient value VC is not well-posed
414            // in QF_BV at that width), so it stays trap-condition-only.
415            WasmOp::I64RemU | WasmOp::I64RemS => {
416                self.verify_i64_rem_value_preservation(wasm_op, arm_ops)
417            }
418            WasmOp::I64DivS | WasmOp::I64DivU => {
419                self.verify_i64_div_rem_trap_preservation(wasm_op, arm_ops)
420            }
421            WasmOp::Unreachable => {
422                let (_state, arm_trap) = self.derive_arm_state(arm_ops, &[], None)?;
423                Ok(Self::condition_verdict(
424                    &crate::trap::trap_always(),
425                    &arm_trap,
426                ))
427            }
428            WasmOp::I32Load { offset, .. }
429            | WasmOp::I32Load8S { offset, .. }
430            | WasmOp::I32Load8U { offset, .. }
431            | WasmOp::I32Load16S { offset, .. }
432            | WasmOp::I32Load16U { offset, .. }
433            | WasmOp::I32Store { offset, .. }
434            | WasmOp::I32Store8 { offset, .. }
435            | WasmOp::I32Store16 { offset, .. } => {
436                let size: u64 = match wasm_op {
437                    WasmOp::I32Load8S { .. }
438                    | WasmOp::I32Load8U { .. }
439                    | WasmOp::I32Store8 { .. } => 1,
440                    WasmOp::I32Load16S { .. }
441                    | WasmOp::I32Load16U { .. }
442                    | WasmOp::I32Store16 { .. } => 2,
443                    _ => 4,
444                };
445                self.verify_mem_trap_preservation(arm_ops, *offset, size)
446            }
447            WasmOp::I32TruncF32S | WasmOp::I32TruncF32U => {
448                let signed = matches!(wasm_op, WasmOp::I32TruncF32S);
449                self.verify_trunc_f32_trap_preservation(arm_ops, signed)
450            }
451            WasmOp::I32TruncF64S | WasmOp::I32TruncF64U => {
452                let signed = matches!(wasm_op, WasmOp::I32TruncF64S);
453                self.verify_trunc_f64_trap_preservation(arm_ops, signed)
454            }
455            other => Err(VerificationError::UnsupportedOperation(format!(
456                "trap-preservation gate does not cover {other:?} \
457                 (i64.trunc_f64 has no shipped lowering — the selector declines \
458                 it; its classifier is unit-gated — see method docs)"
459            ))),
460        }
461    }
462
463    /// i32 div/rem trap preservation (VCR-VER-002, #166): full VC — the trap
464    /// clause AND the guarded value clause — with the ARM trap term DERIVED
465    /// from the emitted guard structure by the branch-taking executor.
466    /// Operands: dividend = `input_0` (R0), divisor = `input_1` (R1),
467    /// result in R0.
468    ///
469    /// The VALUE term comes from the straight-line pass
470    /// ([`ArmSemantics::encode_sequence_value_straightline`]) whenever the
471    /// sequence's branch structure is value-dead
472    /// ([`ArmSemantics::branch_spans_are_value_dead`] — true for every
473    /// shipped div/rem guard shape): on such sequences every non-trapping
474    /// path produces the same registers as straight-line execution, and the
475    /// resulting ite-free SDIV/UDIV/MLS terms stay STRUCTURALLY aligned with
476    /// the WASM encoding — an `ite(guard, …)` wrapper on a divider/multiplier
477    /// operand un-shares the circuits and sends the UNSAT value proof off a
478    /// CDCL cliff. Non-conforming shapes fall back to the guarded
479    /// (if-converted) post-state value — sound, potentially slow.
480    pub fn verify_div_rem_trap_preservation(
481        &self,
482        wasm_op: &WasmOp,
483        arm_ops: &[ArmOp],
484    ) -> Result<ValidationResult, VerificationError> {
485        let Some(div_op) = crate::trap::div_op(wasm_op) else {
486            return Err(VerificationError::UnsupportedOperation(format!(
487                "trap-preservation gate applies to div/rem only, got {wasm_op:?}"
488            )));
489        };
490        // This method models 32-bit operands only; `div_op` also maps the i64
491        // variants (VCR-VER-002 follow-on: i64 needs 64-bit operand terms +
492        // the register-pair ARM value model).
493        if !matches!(
494            wasm_op,
495            WasmOp::I32DivS | WasmOp::I32DivU | WasmOp::I32RemS | WasmOp::I32RemU
496        ) {
497            return Err(VerificationError::UnsupportedOperation(format!(
498                "trap-preservation gate currently supports i32 div/rem only, got {wasm_op:?}"
499            )));
500        }
501
502        // Symbolic operands, matching `verify_equivalence_parameterized`'s
503        // naming: dividend = input_0 (R0), divisor = input_1 (R1).
504        let dividend = BV::new_const("input_0", 32);
505        let divisor = BV::new_const("input_1", 32);
506        let inputs = vec![dividend.clone(), divisor.clone()];
507
508        let wasm_value = self.wasm_encoder.encode_op(wasm_op, &inputs);
509        let (state, arm_may_trap) = self.derive_arm_state(arm_ops, &inputs, None)?;
510        let arm_value = if ArmSemantics::branch_spans_are_value_dead(arm_ops) {
511            // Ite-free value term, structurally aligned with the WASM side
512            // (see the method doc for the soundness argument).
513            let mut vstate = ArmState::new_symbolic();
514            Self::seed_inputs(&mut vstate, &inputs)?;
515            self.arm_encoder
516                .encode_sequence_value_straightline(arm_ops, &mut vstate)
517                .map_err(VerificationError::UnsupportedOperation)?;
518            self.arm_encoder.extract_result(&vstate, &Reg::R0)
519        } else {
520            self.arm_encoder.extract_result(&state, &Reg::R0)
521        };
522
523        let orig = crate::trap::DefineOrTrap {
524            value: wasm_value,
525            may_trap: crate::trap::trap_div(div_op, &dividend, &divisor),
526        };
527        let opt = crate::trap::DefineOrTrap {
528            value: arm_value,
529            may_trap: arm_may_trap,
530        };
531
532        Ok(Self::trap_verdict_to_result(
533            crate::trap::prove_trap_equivalence(&orig, &opt),
534        ))
535    }
536
537    /// i32 load/store OOB trap preservation (VCR-VER-002, #166 / #377):
538    /// trap-condition-only VC (synth models no memory contents). The WASM
539    /// side is ordeal's wraparound-safe bound check on the effective address
540    /// (`addr + offset + size >u mem_size`, exact 33-bit arithmetic); the ARM
541    /// side is DERIVED from the emitted software-bounds guard. Operand
542    /// convention: address = `input_0` (R0), store value (if any) = `input_1`
543    /// (R1), linear-memory size = R10 (the shipped ABI register).
544    pub fn verify_mem_trap_preservation(
545        &self,
546        arm_ops: &[ArmOp],
547        offset: u32,
548        access_size: u64,
549    ) -> Result<ValidationResult, VerificationError> {
550        let addr = BV::new_const("input_0", 32);
551        let value = BV::new_const("input_1", 32);
552        let inputs = vec![addr.clone(), value];
553
554        let mut state = ArmState::new_symbolic();
555        // The WASM-side bound is THE SAME symbol the ARM guard compares
556        // against: R10 = linear-memory size in bytes (shipped ABI).
557        let mem_bound = state.get_reg(&Reg::R10).clone();
558        Self::seed_inputs(&mut state, &inputs)?;
559        self.arm_encoder
560            .encode_sequence_br(arm_ops, &mut state)
561            .map_err(VerificationError::UnsupportedOperation)?;
562        let arm_trap = state.may_trap.clone();
563
564        // WASM Core: ea = addr + offset (exact), trap iff ea + size > bound.
565        // Folding the static offset into the size operand keeps ordeal's
566        // 33-bit zero-extended arithmetic exact: zext(addr) + zext(off+size).
567        let static_bytes = offset as u64 + access_size;
568        let wasm_trap = if static_bytes > u32::MAX as u64 {
569            // offset + size alone exceeds the 32-bit bound: every access traps.
570            crate::trap::trap_always()
571        } else {
572            crate::trap::trap_mem_oob(&addr, &BV::from_u64(static_bytes, 32), &mem_bound)
573        };
574
575        Ok(Self::condition_verdict(&wasm_trap, &arm_trap))
576    }
577
578    /// `i32.trunc_f32_s/u` trap preservation (VCR-VER-002, #166 / #709):
579    /// trap-condition-only VC (synth's QF_BV model carries no float→int
580    /// value function). The WASM side is ordeal 0.9.1's bit-pattern trunc
581    /// classifier (`NaN ∨ ±∞ ∨ out-of-range`); the ARM side is DERIVED from
582    /// the emitted domain guard (`F32Const` bound + ordered VFP compare +
583    /// `Cmp`/branch/`Udf`), with the ordered compares given real bit-pattern
584    /// semantics in the executor. Operand convention: float operand = S0.
585    pub fn verify_trunc_f32_trap_preservation(
586        &self,
587        arm_ops: &[ArmOp],
588        signed: bool,
589    ) -> Result<ValidationResult, VerificationError> {
590        use synth_synthesis::rules::VfpReg;
591        let bits = BV::new_const("input_0", 32);
592
593        let mut state = ArmState::new_symbolic();
594        state.set_vfp_reg(&VfpReg::S0, bits.clone());
595        self.arm_encoder
596            .encode_sequence_br(arm_ops, &mut state)
597            .map_err(VerificationError::UnsupportedOperation)?;
598        let arm_trap = state.may_trap.clone();
599
600        let wasm_trap = crate::trap::trap_trunc(
601            &bits,
602            crate::trap::FpFmt::F32,
603            crate::trap::IntTarget::I32,
604            signed,
605        );
606
607        Ok(Self::condition_verdict(&wasm_trap, &arm_trap))
608    }
609
610    /// i64 div/rem trap preservation (VCR-VER-002, #756): field-derived
611    /// trap-condition-only VC. ARM32 has no 64-bit divide, so the shipped
612    /// lowering emits a single `ArmOp::I64Div{S,U}`/`I64Rem{S,U}` pseudo-op
613    /// whose 64-bit quotient the exec-model leaves symbolic (no `bvsdiv`
614    /// term — the full value VC is not well-posed) and whose trap guards are
615    /// carried as `elide_*` boolean FIELDS. The WASM spec side is ordeal's
616    /// 64-bit `trap_div` over symbolic divisor bits (`rmlo:rmhi` = R2:R3); the
617    /// ARM side is CONSTRUCTED from the pseudo-op's fields — a set `elide_*`
618    /// deletes the corresponding clause — so a lowering that elides a guard
619    /// (÷0 for all four, INT64_MIN/-1 overflow for `div_s`) whose fact was not
620    /// discharged derives a weaker trap term and is reported `Invalid`.
621    pub fn verify_i64_div_rem_trap_preservation(
622        &self,
623        wasm_op: &WasmOp,
624        arm_ops: &[ArmOp],
625    ) -> Result<ValidationResult, VerificationError> {
626        let Some(div_op) = crate::trap::div_op(wasm_op) else {
627            return Err(VerificationError::UnsupportedOperation(format!(
628                "i64 trap-preservation gate applies to div/rem only, got {wasm_op:?}"
629            )));
630        };
631        if !matches!(
632            wasm_op,
633            WasmOp::I64DivS | WasmOp::I64DivU | WasmOp::I64RemS | WasmOp::I64RemU
634        ) {
635            return Err(VerificationError::UnsupportedOperation(format!(
636                "i64 trap-preservation gate supports i64 div/rem only, got {wasm_op:?}"
637            )));
638        }
639
640        // The shipped lowering is a single pseudo-op; find it in the sequence
641        // (the selector emits exactly one, possibly amid dead ops).
642        let (elide_zero, elide_overflow) =
643            Self::i64_div_rem_guard_fields(arm_ops).ok_or_else(|| {
644                VerificationError::UnsupportedOperation(format!(
645                    "i64 trap gate needs an I64Div/I64Rem pseudo-op in the sequence, \
646                     got {arm_ops:?}"
647                ))
648            })?;
649
650        // WASM spec side: full 64-bit trap condition (÷0 ∨, for div_s only,
651        // INT64_MIN/-1 overflow). The dividend/divisor symbols name the
652        // register pairs the pseudo-op reads (rnlo:rnhi = R0:R1 dividend,
653        // rmlo:rmhi = R2:R3 divisor).
654        let dividend = BV::new_const("input_dividend_i64", 64);
655        let divisor = BV::new_const("input_divisor_i64", 64);
656        let wasm_trap = crate::trap::trap_div(div_op, &dividend, &divisor);
657
658        // ARM side: reconstruct the SAME 64-bit trap term but with each clause
659        // CONDITIONALLY present per the pseudo-op's elision fields. A dropped
660        // guard => a strictly weaker term => the equivalence VC is Sat.
661        let arm_trap =
662            Self::i64_arm_trap_from_fields(div_op, &dividend, &divisor, elide_zero, elide_overflow);
663
664        Ok(Self::condition_verdict(&wasm_trap, &arm_trap))
665    }
666
667    /// i64 `rem_u`/`rem_s` **value + trap** preservation (VCR-VER, #825/#836).
668    ///
669    /// Unlike [`Self::verify_i64_div_rem_trap_preservation`] (trap-only, used
670    /// for div where the 64-bit quotient value VC is not well-posed), this
671    /// gate asserts the FULL obligation for remainder: on the non-trapping
672    /// path the ARM register pair `R0:R1` must equal the native 64-bit
673    /// unsigned/signed remainder of the operands, AND the ÷0 trap must be
674    /// preserved. It is what makes the [`ArmSemantics`] `I64RemU` model —
675    /// which now builds a real `BvTerm::Urem` term (ordeal 0.12) instead of
676    /// HAVOC — actually load-bearing: a wrong remainder computation, or one
677    /// that writes the answer to the wrong register pair, is now `Invalid`.
678    ///
679    /// # Operand / result convention (the shipped ABI, NOT the op's fields)
680    ///
681    /// The value is READ from the FIXED ABI return registers `R0` (low) /
682    /// `R1` (high) and the operands are SEEDED at the FIXED ABI argument
683    /// registers — dividend `R0:R1`, divisor `R2:R3`. Reading the op's own
684    /// `rd*` fields (or seeding its own `rn*`/`rm*`) would make the VC
685    /// vacuous (read tracks write); anchoring on the ABI is exactly what lets
686    /// a pseudo-op whose destination deviates from `R0:R1` be REJECTED.
687    ///
688    /// # Trap-guarded value (why not a raw `==`)
689    ///
690    /// A bare `arm_value == wasm_value` would also assert the value on the ÷0
691    /// path, where WASM has no value and SMT-LIB `bvurem`-by-0 = the dividend
692    /// (total) — a spurious obligation. Routing through
693    /// [`crate::trap::DefineOrTrap`] + [`crate::trap::prove_trap_equivalence`]
694    /// guards the value clause under non-trap. The single branch-free
695    /// pseudo-op keeps the value ite-free, so no straight-line rewrite is
696    /// needed (unlike the i32 guarded-div machinery).
697    pub fn verify_i64_rem_value_preservation(
698        &self,
699        wasm_op: &WasmOp,
700        arm_ops: &[ArmOp],
701    ) -> Result<ValidationResult, VerificationError> {
702        let Some(div_op) = crate::trap::div_op(wasm_op) else {
703            return Err(VerificationError::UnsupportedOperation(format!(
704                "i64 rem value gate applies to rem only, got {wasm_op:?}"
705            )));
706        };
707        if !matches!(wasm_op, WasmOp::I64RemU | WasmOp::I64RemS) {
708            return Err(VerificationError::UnsupportedOperation(format!(
709                "i64 rem value gate supports i64 rem_u/rem_s only, got {wasm_op:?}"
710            )));
711        }
712
713        // Require the pseudo-op to be present and read its ÷0 guard-elision
714        // field (the selector emits exactly one). The ARM trap term is
715        // reconstructed from this field — NOT from `encode_sequence_br`'s
716        // `may_trap`, which is `false` for a bare pseudo-op (it carries no
717        // expanded `UDF`) — exactly as the i64 trap-only VC does. A lowering
718        // that elides the ÷0 guard without discharging `divisor != 0` yields a
719        // strictly weaker ARM trap and is rejected.
720        let Some((elide_zero, _elide_overflow)) = Self::i64_div_rem_guard_fields(arm_ops) else {
721            return Err(VerificationError::UnsupportedOperation(format!(
722                "i64 rem value gate needs an I64Rem pseudo-op in the sequence, \
723                 got {arm_ops:?}"
724            )));
725        };
726
727        // Four independent 32-bit half-symbols. Seed them at the FIXED ABI
728        // argument registers: dividend lo:hi = R0:R1, divisor lo:hi = R2:R3.
729        let dividend_lo = BV::new_const("input_dividend_lo", 32);
730        let dividend_hi = BV::new_const("input_dividend_hi", 32);
731        let divisor_lo = BV::new_const("input_divisor_lo", 32);
732        let divisor_hi = BV::new_const("input_divisor_hi", 32);
733
734        let mut state = ArmState::new_symbolic();
735        state.set_reg(&Reg::R0, dividend_lo.clone());
736        state.set_reg(&Reg::R1, dividend_hi.clone());
737        state.set_reg(&Reg::R2, divisor_lo.clone());
738        state.set_reg(&Reg::R3, divisor_hi.clone());
739        self.arm_encoder
740            .encode_sequence_br(arm_ops, &mut state)
741            .map_err(VerificationError::UnsupportedOperation)?;
742
743        // ARM result read from the FIXED ABI return pair R0:R1 (concat puts
744        // self in the HIGH bits).
745        let arm_lo = self.arm_encoder.extract_result(&state, &Reg::R0);
746        let arm_hi = self.arm_encoder.extract_result(&state, &Reg::R1);
747        let arm_value = arm_hi.concat(&arm_lo); // 64-bit R1:R0
748
749        // WASM spec side: the 64-bit remainder of the SAME operand symbols.
750        // Both sides feed identical concats, so the valid case is structural.
751        let dividend = dividend_hi.concat(&dividend_lo); // 64-bit
752        let divisor = divisor_hi.concat(&divisor_lo); // 64-bit
753        let wasm_value = match wasm_op {
754            WasmOp::I64RemU => dividend.bvurem(&divisor),
755            WasmOp::I64RemS => dividend.bvsrem(&divisor),
756            _ => unreachable!("guarded above"),
757        };
758        let wasm_trap = crate::trap::trap_div(div_op, &dividend, &divisor);
759
760        // ARM trap: rem's only trap is ÷0; present iff the guard was NOT
761        // elided. rem carries no overflow clause, so `elide_overflow=false`.
762        let arm_may_trap = Self::i64_arm_trap_from_fields(
763            div_op, &dividend, &divisor, elide_zero, /* elide_overflow */ false,
764        );
765
766        let orig = crate::trap::DefineOrTrap {
767            value: wasm_value,
768            may_trap: wasm_trap,
769        };
770        let opt = crate::trap::DefineOrTrap {
771            value: arm_value,
772            may_trap: arm_may_trap,
773        };
774
775        Ok(Self::trap_verdict_to_result(
776            crate::trap::prove_trap_equivalence(&orig, &opt),
777        ))
778    }
779
780    /// Locate the i64 div/rem pseudo-op in a sequence and read its guard-elision
781    /// fields as `(elide_zero, elide_overflow)`. `div_u`/`rem_s`/`rem_u` have no
782    /// overflow guard, so their `elide_overflow` is reported `false`
783    /// (the clause is not part of their WASM trap condition anyway).
784    fn i64_div_rem_guard_fields(arm_ops: &[ArmOp]) -> Option<(bool, bool)> {
785        arm_ops.iter().find_map(|op| match op {
786            ArmOp::I64DivS {
787                elide_zero_guard,
788                elide_overflow_guard,
789                ..
790            } => Some((*elide_zero_guard, *elide_overflow_guard)),
791            ArmOp::I64DivU {
792                elide_zero_guard, ..
793            }
794            | ArmOp::I64RemS {
795                elide_zero_guard, ..
796            }
797            | ArmOp::I64RemU {
798                elide_zero_guard, ..
799            } => Some((*elide_zero_guard, false)),
800            _ => None,
801        })
802    }
803
804    /// Build the ARM-side i64 trap term from the guard-elision fields: start
805    /// from the full WASM trap condition and DELETE each clause the pseudo-op
806    /// elides. The ÷0 clause is `divisor == 0`; the overflow clause (`div_s`
807    /// only) is `dividend == INT64_MIN ∧ divisor == -1`.
808    fn i64_arm_trap_from_fields(
809        div_op: crate::trap::DivOp,
810        dividend: &BV,
811        divisor: &BV,
812        elide_zero: bool,
813        elide_overflow: bool,
814    ) -> Bool {
815        let zero = BV::from_u64(0, 64);
816        let div_by_zero = divisor.eq(&zero);
817
818        // The ÷0 clause is present iff not elided.
819        let mut clauses: Vec<Bool> = Vec::new();
820        if !elide_zero {
821            clauses.push(div_by_zero);
822        }
823
824        // The overflow clause is only part of div_s's WASM condition; for the
825        // other three it is not in `trap_div` at all, so never add it.
826        if matches!(div_op, crate::trap::DivOp::DivS) && !elide_overflow {
827            let int_min = BV::from_i64(i64::MIN, 64);
828            let neg_one = BV::from_i64(-1, 64);
829            let overflow = Bool::and(&[&dividend.eq(&int_min), &divisor.eq(&neg_one)]);
830            clauses.push(overflow);
831        }
832
833        if clauses.is_empty() {
834            Bool::from_bool(false)
835        } else {
836            let refs: Vec<&Bool> = clauses.iter().collect();
837            Bool::or(&refs)
838        }
839    }
840
841    /// `i32.trunc_f64_s/u` trap preservation (VCR-VER-002, #166 / #709 / #756):
842    /// trap-condition-only VC (synth's QF_BV model carries no float→int value
843    /// function). The WASM side is ordeal's bit-pattern trunc classifier over
844    /// the f64 operand (`NaN ∨ ±∞ ∨ out-of-range`); the ARM side is DERIVED
845    /// from the emitted DOUBLE-precision domain guard (`F64Const` bound +
846    /// ordered `VCMP.F64` compare + `Cmp`/branch/`Udf`), with the ordered f64
847    /// compares given real bit-pattern semantics in the executor. Operand
848    /// convention: f64 operand = D0 (a 64-bit BV), bound scratch = D1.
849    pub fn verify_trunc_f64_trap_preservation(
850        &self,
851        arm_ops: &[ArmOp],
852        signed: bool,
853    ) -> Result<ValidationResult, VerificationError> {
854        use synth_synthesis::rules::VfpReg;
855        let bits = BV::new_const("input_0", 64);
856
857        let mut state = ArmState::new_symbolic();
858        state.set_vfp_reg(&VfpReg::D0, bits.clone());
859        self.arm_encoder
860            .encode_sequence_br(arm_ops, &mut state)
861            .map_err(VerificationError::UnsupportedOperation)?;
862        let arm_trap = state.may_trap.clone();
863
864        let wasm_trap = crate::trap::trap_trunc(
865            &bits,
866            crate::trap::FpFmt::F64,
867            crate::trap::IntTarget::I32,
868            signed,
869        );
870
871        Ok(Self::condition_verdict(&wasm_trap, &arm_trap))
872    }
873
874    /// `call_indirect` trap preservation (VCR-VER-002, #166 / #642 #664 #676):
875    /// trap-condition-only VC. The WASM-side spec comes from MODULE FACTS the
876    /// caller supplies ([`CallIndirectSpec`]); the ARM side is derived from
877    /// the `ArmOp::CallIndirect` pseudo-op's guard fields (`table_size`,
878    /// `null_check`, `type_check`) through the SAME pinned ordeal builder —
879    /// a selector that resolves the wrong table size, drops the null check on
880    /// a table with uninitialized slots (#664), or skips the runtime type
881    /// check on a heterogeneous table (#676) is reported `Invalid`.
882    ///
883    /// # Trust boundary
884    ///
885    /// This certifies the SELECTOR's guard resolution at the pseudo-op level;
886    /// the encoder's expansion of those fields into `CMP`/`BLO`/`UDF`/`BLX`
887    /// bytes is separately execution-gated (the unicorn call_indirect CI
888    /// jobs). Statically-discharged clauses are modeled by a provably
889    /// non-null slot term (`slot | 1`), keeping ordeal's builder the single
890    /// spec source.
891    pub fn verify_call_indirect_trap_preservation(
892        &self,
893        arm_op: &ArmOp,
894        spec: &CallIndirectSpec,
895    ) -> Result<ValidationResult, VerificationError> {
896        let ArmOp::CallIndirect {
897            table_size,
898            null_check,
899            type_check,
900            ..
901        } = arm_op
902        else {
903            return Err(VerificationError::UnsupportedOperation(format!(
904                "call_indirect trap gate needs the CallIndirect pseudo-op, got {arm_op:?}"
905            )));
906        };
907
908        let index = BV::new_const("input_0", 32);
909        let slot = BV::new_const("slot_ptr", 32);
910        let nonnull_slot = slot.bvor(BV::from_u64(1, 32));
911        let actual_ty = BV::new_const("slot_type_id", 32);
912
913        let build = |size: u32, may_null: bool, expected: Option<u32>| {
914            let expected_bv = expected.map(|e| BV::from_u64(e as u64, 32));
915            let size_bv = BV::from_u64(size as u64, 32);
916            let slot_term = if may_null { &slot } else { &nonnull_slot };
917            let type_trap = match &expected_bv {
918                Some(e) => crate::trap::TypeTrap::Runtime {
919                    actual_type_id: &actual_ty,
920                    expected_id: e,
921                },
922                None => crate::trap::TypeTrap::StaticallyDischarged,
923            };
924            crate::trap::trap_call_indirect(&crate::trap::CallIndirect {
925                index: &index,
926                table_size: &size_bv,
927                slot_ptr: slot_term,
928                type_trap,
929            })
930        };
931
932        let wasm_trap = build(
933            spec.table_size,
934            spec.may_have_null_slot,
935            spec.heterogeneous_expected_type,
936        );
937        let arm_trap = build(
938            *table_size,
939            *null_check,
940            type_check.as_ref().map(|(expected, _)| *expected),
941        );
942
943        Ok(Self::condition_verdict(&wasm_trap, &arm_trap))
944    }
945
946    /// Seed integer operand registers (R0..R2) and run the branch-taking
947    /// executor, returning the post-state and the DERIVED trap condition.
948    fn derive_arm_state(
949        &self,
950        arm_ops: &[ArmOp],
951        inputs: &[BV],
952        vfp_s0: Option<&BV>,
953    ) -> Result<(ArmState, Bool), VerificationError> {
954        let mut state = ArmState::new_symbolic();
955        Self::seed_inputs(&mut state, inputs)?;
956        if let Some(bits) = vfp_s0 {
957            state.set_vfp_reg(&synth_synthesis::rules::VfpReg::S0, bits.clone());
958        }
959        self.arm_encoder
960            .encode_sequence_br(arm_ops, &mut state)
961            .map_err(VerificationError::UnsupportedOperation)?;
962        let trap = state.may_trap.clone();
963        Ok((state, trap))
964    }
965
966    fn seed_inputs(state: &mut ArmState, inputs: &[BV]) -> Result<(), VerificationError> {
967        for (i, input) in inputs.iter().enumerate() {
968            let reg = match i {
969                0 => Reg::R0,
970                1 => Reg::R1,
971                2 => Reg::R2,
972                _ => {
973                    return Err(VerificationError::UnsupportedOperation(format!(
974                        "Too many inputs: {}",
975                        inputs.len()
976                    )));
977                }
978            };
979            state.set_reg(&reg, input.clone());
980        }
981        Ok(())
982    }
983
984    /// Run the trap-condition-only VC and map the verdict.
985    fn condition_verdict(wasm_trap: &Bool, arm_trap: &Bool) -> ValidationResult {
986        Self::trap_verdict_to_result(crate::trap::prove_trap_condition_equivalence(
987            wasm_trap, arm_trap,
988        ))
989    }
990
991    fn trap_verdict_to_result(verdict: crate::trap::TrapVerdict) -> ValidationResult {
992        match verdict {
993            crate::trap::TrapVerdict::Preserved => ValidationResult::Verified,
994            crate::trap::TrapVerdict::Dropped(model) => ValidationResult::Invalid {
995                counterexample: model
996                    .into_iter()
997                    .filter_map(|(n, v)| i64::try_from(v).ok().map(|x| (n, x)))
998                    .collect(),
999            },
1000            crate::trap::TrapVerdict::Unknown => ValidationResult::Unknown {
1001                reason: "trap-preservation VC returned Unknown (conservative reject)".to_string(),
1002            },
1003        }
1004    }
1005
1006    /// Get number of inputs required for a WASM operation
1007    fn get_num_inputs(&self, wasm_op: &WasmOp) -> usize {
1008        use WasmOp::*;
1009        match wasm_op {
1010            // Binary operations
1011            I32Add | I32Sub | I32Mul | I32DivS | I32DivU | I32RemS | I32RemU | I32And | I32Or
1012            | I32Xor | I32Shl | I32ShrS | I32ShrU | I32Rotl | I32Rotr | I32Eq | I32Ne | I32LtS
1013            | I32LtU | I32LeS | I32LeU | I32GtS | I32GtU | I32GeS | I32GeU => 2,
1014
1015            // Unary operations
1016            I32Clz | I32Ctz | I32Popcnt | I32Eqz => 1,
1017
1018            // Constants
1019            I32Const(_) => 0,
1020
1021            // Memory operations
1022            I32Load { .. } => 1,  // address
1023            I32Store { .. } => 2, // address + value
1024
1025            // Control flow
1026            LocalGet(_) | GlobalGet(_) => 0,
1027            LocalSet(_) | GlobalSet(_) | LocalTee(_) => 1,
1028            Br(_) | BrIf(_) | Return => 0,
1029
1030            // Other operations
1031            Drop => 1,
1032            Select => 3, // condition + two values
1033            Nop | Unreachable | Block | Loop | If | Else | End => 0,
1034
1035            // Default for unknown
1036            _ => 0,
1037        }
1038    }
1039
1040    /// Batch verify multiple synthesis rules
1041    pub fn verify_rules(
1042        &self,
1043        rules: &[SynthesisRule],
1044    ) -> Vec<(String, Result<ValidationResult, VerificationError>)> {
1045        rules
1046            .iter()
1047            .map(|rule| {
1048                let result = self.verify_rule(rule);
1049                (rule.name.clone(), result)
1050            })
1051            .collect()
1052    }
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057    use super::*;
1058    use crate::with_verification_context;
1059    use synth_synthesis::rules::Condition;
1060    use synth_synthesis::{Cost, Operand2, Pattern, Replacement};
1061
1062    fn create_test_rule(wasm_op: WasmOp, arm_op: ArmOp) -> SynthesisRule {
1063        SynthesisRule {
1064            name: format!("{:?}", wasm_op),
1065            priority: 0,
1066            pattern: Pattern::WasmInstr(wasm_op),
1067            replacement: Replacement::ArmInstr(arm_op),
1068            cost: Cost {
1069                cycles: 1,
1070                code_size: 4,
1071                registers: 2,
1072            },
1073        }
1074    }
1075
1076    // --- VCR-VER-002 (#166): div/rem trap-preservation wired into the validator ---
1077
1078    #[test]
1079    fn div_lowering_without_guard_is_rejected_as_trap_drop() {
1080        with_verification_context(|| {
1081            let validator = TranslationValidator::new();
1082            // Bare UDIV — the value is right but the ÷0 guard is missing
1083            // (the #633/#666 shape). The trap-preservation gate must reject it.
1084            let arm_ops = [ArmOp::Udiv {
1085                rd: Reg::R0,
1086                rn: Reg::R0,
1087                rm: Reg::R1,
1088            }];
1089            let result = validator
1090                .verify_div_rem_trap_preservation(&WasmOp::I32DivU, &arm_ops)
1091                .unwrap();
1092            match result {
1093                ValidationResult::Invalid { counterexample } => {
1094                    // The counterexample must exhibit the dropped trap: divisor 0.
1095                    let divisor = counterexample.iter().find(|(n, _)| n == "input_1");
1096                    assert_eq!(
1097                        divisor.map(|(_, v)| *v),
1098                        Some(0),
1099                        "trap-drop counterexample must set the divisor to 0"
1100                    );
1101                }
1102                other => panic!("unguarded div must be Invalid, got {other:?}"),
1103            }
1104        });
1105    }
1106
1107    /// The SHIPPED ÷0 guard shape (instruction_selector.rs I32DivU /
1108    /// optimizer_bridge.rs DivU): CMP divisor,#0 ; BNE +0 (skip the UDF) ;
1109    /// UDF ; UDIV. The derived trap term is (divisor == 0) — exactly WASM's.
1110    fn shipped_divu_guard() -> Vec<ArmOp> {
1111        vec![
1112            ArmOp::Cmp {
1113                rn: Reg::R1,
1114                op2: Operand2::Imm(0),
1115            },
1116            ArmOp::BCondOffset {
1117                cond: Condition::NE,
1118                offset: 0,
1119            },
1120            ArmOp::Udf { imm: 0 },
1121            ArmOp::Udiv {
1122                rd: Reg::R0,
1123                rn: Reg::R0,
1124                rm: Reg::R1,
1125            },
1126        ]
1127    }
1128
1129    /// The SHIPPED div_s DOUBLE guard (optimizer_bridge.rs DivS): the ÷0
1130    /// guard plus the INT_MIN/-1 overflow guard (MOVW/MOVT 0x80000000 into
1131    /// R12 ; CMP dividend ; BNE +3 ; CMN divisor,#1 ; BNE +0 ; UDF #1).
1132    fn shipped_divs_double_guard() -> Vec<ArmOp> {
1133        vec![
1134            ArmOp::Cmp {
1135                rn: Reg::R1,
1136                op2: Operand2::Imm(0),
1137            },
1138            ArmOp::BCondOffset {
1139                cond: Condition::NE,
1140                offset: 0,
1141            },
1142            ArmOp::Udf { imm: 0 },
1143            ArmOp::Movw {
1144                rd: Reg::R12,
1145                imm16: 0,
1146            },
1147            ArmOp::Movt {
1148                rd: Reg::R12,
1149                imm16: 0x8000,
1150            },
1151            ArmOp::Cmp {
1152                rn: Reg::R0,
1153                op2: Operand2::Reg(Reg::R12),
1154            },
1155            ArmOp::BCondOffset {
1156                cond: Condition::NE,
1157                offset: 3,
1158            },
1159            ArmOp::Cmn {
1160                rn: Reg::R1,
1161                op2: Operand2::Imm(1),
1162            },
1163            ArmOp::BCondOffset {
1164                cond: Condition::NE,
1165                offset: 0,
1166            },
1167            ArmOp::Udf { imm: 1 },
1168            ArmOp::Sdiv {
1169                rd: Reg::R0,
1170                rn: Reg::R0,
1171                rm: Reg::R1,
1172            },
1173        ]
1174    }
1175
1176    #[test]
1177    fn div_lowering_with_guard_preserves_the_trap() {
1178        with_verification_context(|| {
1179            let validator = TranslationValidator::new();
1180            let result = validator
1181                .verify_div_rem_trap_preservation(&WasmOp::I32DivU, &shipped_divu_guard())
1182                .unwrap();
1183            assert_eq!(result, ValidationResult::Verified);
1184        });
1185    }
1186
1187    /// The derived gate is STRICTER than the retired Udf-presence proxy: a
1188    /// guard with the branch polarity inverted (BEQ instead of BNE — the UDF
1189    /// fires exactly when the divide is fine) still contains a Udf, so the
1190    /// proxy called it Verified; the derived trap term is (divisor != 0),
1191    /// which fails the VC.
1192    #[test]
1193    fn div_guard_with_inverted_polarity_is_rejected() {
1194        with_verification_context(|| {
1195            let validator = TranslationValidator::new();
1196            let mut arm_ops = shipped_divu_guard();
1197            arm_ops[1] = ArmOp::BCondOffset {
1198                cond: Condition::EQ,
1199                offset: 0,
1200            };
1201            let result = validator
1202                .verify_div_rem_trap_preservation(&WasmOp::I32DivU, &arm_ops)
1203                .unwrap();
1204            assert!(
1205                matches!(result, ValidationResult::Invalid { .. }),
1206                "inverted guard polarity must be Invalid, got {result:?}"
1207            );
1208        });
1209    }
1210
1211    #[test]
1212    fn signed_div_double_guard_preserves_both_zero_and_overflow_traps() {
1213        with_verification_context(|| {
1214            let validator = TranslationValidator::new();
1215            let result = validator
1216                .verify_div_rem_trap_preservation(&WasmOp::I32DivS, &shipped_divs_double_guard())
1217                .unwrap();
1218            assert_eq!(result, ValidationResult::Verified);
1219        });
1220    }
1221
1222    /// RED-FIRST for the div_s overflow class (#633-shape at i32): stripping
1223    /// ONLY the INT_MIN/-1 overflow guard keeps a Udf in the sequence (the ÷0
1224    /// guard), so the retired structural proxy called this Verified. The
1225    /// derived trap term is only (divisor == 0), and the VC finds the dropped
1226    /// overflow trap with the INT_MIN/-1 counterexample.
1227    #[test]
1228    fn signed_div_with_overflow_guard_stripped_is_rejected() {
1229        with_verification_context(|| {
1230            let validator = TranslationValidator::new();
1231            let arm_ops = [
1232                ArmOp::Cmp {
1233                    rn: Reg::R1,
1234                    op2: Operand2::Imm(0),
1235                },
1236                ArmOp::BCondOffset {
1237                    cond: Condition::NE,
1238                    offset: 0,
1239                },
1240                ArmOp::Udf { imm: 0 },
1241                ArmOp::Sdiv {
1242                    rd: Reg::R0,
1243                    rn: Reg::R0,
1244                    rm: Reg::R1,
1245                },
1246            ];
1247            let result = validator
1248                .verify_div_rem_trap_preservation(&WasmOp::I32DivS, &arm_ops)
1249                .unwrap();
1250            match result {
1251                ValidationResult::Invalid { counterexample } => {
1252                    let get = |n: &str| {
1253                        counterexample
1254                            .iter()
1255                            .find(|(name, _)| name == n)
1256                            .map(|(_, v)| *v)
1257                    };
1258                    assert_eq!(
1259                        get("input_0"),
1260                        Some(i32::MIN as u32 as i64),
1261                        "dropped overflow trap must exhibit dividend INT_MIN: {counterexample:?}"
1262                    );
1263                    assert_eq!(
1264                        get("input_1"),
1265                        Some(u32::MAX as i64),
1266                        "dropped overflow trap must exhibit divisor -1: {counterexample:?}"
1267                    );
1268                }
1269                other => panic!("overflow-guard-stripped div_s must be Invalid, got {other:?}"),
1270            }
1271        });
1272    }
1273
1274    /// rem_s carries ONLY the ÷0 guard — WASM rem_s(INT_MIN, -1) is 0, not a
1275    /// trap — and the derived gate agrees.
1276    #[test]
1277    fn rems_single_zero_guard_is_exactly_right() {
1278        with_verification_context(|| {
1279            let validator = TranslationValidator::new();
1280            let arm_ops = [
1281                ArmOp::Cmp {
1282                    rn: Reg::R1,
1283                    op2: Operand2::Imm(0),
1284                },
1285                ArmOp::BCondOffset {
1286                    cond: Condition::NE,
1287                    offset: 0,
1288                },
1289                ArmOp::Udf { imm: 0 },
1290                ArmOp::Sdiv {
1291                    rd: Reg::R2,
1292                    rn: Reg::R0,
1293                    rm: Reg::R1,
1294                },
1295                ArmOp::Mls {
1296                    rd: Reg::R0,
1297                    rn: Reg::R2,
1298                    rm: Reg::R1,
1299                    ra: Reg::R0,
1300                },
1301            ];
1302            let result = validator
1303                .verify_div_rem_trap_preservation(&WasmOp::I32RemS, &arm_ops)
1304                .unwrap();
1305            assert_eq!(result, ValidationResult::Verified);
1306        });
1307    }
1308
1309    // --- unreachable (LIVE, #665 class) ---
1310
1311    #[test]
1312    fn unreachable_udf_lowering_preserves_the_trap() {
1313        with_verification_context(|| {
1314            let validator = TranslationValidator::new();
1315            let result = validator
1316                .verify_trap_preservation(&WasmOp::Unreachable, &[ArmOp::Udf { imm: 0 }])
1317                .unwrap();
1318            assert_eq!(result, ValidationResult::Verified);
1319        });
1320    }
1321
1322    #[test]
1323    fn unreachable_lowered_to_nop_is_rejected() {
1324        with_verification_context(|| {
1325            let validator = TranslationValidator::new();
1326            // The #665 shape: unreachable silently became a no-op.
1327            let result = validator
1328                .verify_trap_preservation(&WasmOp::Unreachable, &[ArmOp::Nop])
1329                .unwrap();
1330            assert!(
1331                matches!(result, ValidationResult::Invalid { .. }),
1332                "trap-dropping unreachable lowering must be Invalid, got {result:?}"
1333            );
1334        });
1335    }
1336
1337    // --- i32 load/store OOB (LIVE, #377/#752 class) ---
1338
1339    /// The SHIPPED software-bounds lowering at the gate's register
1340    /// convention (address = R0 = `input_0`): the guard prefix comes from
1341    /// `InstructionSelector::software_bounds_guard` — THE function every
1342    /// emission site calls, public precisely so this gate pins the real
1343    /// shipped shape and no hand-maintained mirror can drift (the VCR-ORACLE
1344    /// lesson) — composed with the trailing access op per the generators'
1345    /// documented contract (`result.last() is Ldr/Ldrb/.../Str
1346    /// [R11, addr, #offset]`, pinned by the selector unit tests). The full
1347    /// selector is not used directly because it homes a bare op's address in
1348    /// an allocator-chosen register, not the gate's `input_0`.
1349    fn shipped_software_bounds_ops(wasm_op: &WasmOp) -> Vec<ArmOp> {
1350        use synth_synthesis::instruction_selector::InstructionSelector;
1351        use synth_synthesis::rules::MemAddr;
1352        let (offset, size) = match wasm_op {
1353            WasmOp::I32Load { offset, .. } | WasmOp::I32Store { offset, .. } => (*offset, 4u32),
1354            WasmOp::I32Load16S { offset, .. }
1355            | WasmOp::I32Load16U { offset, .. }
1356            | WasmOp::I32Store16 { offset, .. } => (*offset, 2),
1357            WasmOp::I32Load8S { offset, .. }
1358            | WasmOp::I32Load8U { offset, .. }
1359            | WasmOp::I32Store8 { offset, .. } => (*offset, 1),
1360            other => panic!("not a guarded i32 access: {other:?}"),
1361        };
1362        let addr = MemAddr::reg_imm(Reg::R11, Reg::R0, offset as i32);
1363        let access = match wasm_op {
1364            WasmOp::I32Load { .. } => ArmOp::Ldr { rd: Reg::R0, addr },
1365            WasmOp::I32Load8S { .. } => ArmOp::Ldrsb { rd: Reg::R0, addr },
1366            WasmOp::I32Load8U { .. } => ArmOp::Ldrb { rd: Reg::R0, addr },
1367            WasmOp::I32Load16S { .. } => ArmOp::Ldrsh { rd: Reg::R0, addr },
1368            WasmOp::I32Load16U { .. } => ArmOp::Ldrh { rd: Reg::R0, addr },
1369            WasmOp::I32Store { .. } => ArmOp::Str { rd: Reg::R1, addr },
1370            WasmOp::I32Store8 { .. } => ArmOp::Strb { rd: Reg::R1, addr },
1371            WasmOp::I32Store16 { .. } => ArmOp::Strh { rd: Reg::R1, addr },
1372            other => panic!("not a guarded i32 access: {other:?}"),
1373        };
1374        let mut ops = InstructionSelector::software_bounds_guard(Reg::R0, offset as i32, size);
1375        ops.push(access);
1376        ops
1377    }
1378
1379    /// RED-FIRST: a load with the bounds guard stripped derives trap = false
1380    /// and is rejected against the WASM OOB condition.
1381    #[test]
1382    fn load_without_bounds_guard_is_rejected() {
1383        use synth_synthesis::rules::MemAddr;
1384        with_verification_context(|| {
1385            let validator = TranslationValidator::new();
1386            let arm_ops = [ArmOp::Ldr {
1387                rd: Reg::R0,
1388                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
1389            }];
1390            let result = validator
1391                .verify_trap_preservation(
1392                    &WasmOp::I32Load {
1393                        offset: 0,
1394                        align: 2,
1395                    },
1396                    &arm_ops,
1397                )
1398                .unwrap();
1399            assert!(
1400                matches!(result, ValidationResult::Invalid { .. }),
1401                "guard-stripped load must be Invalid, got {result:?}"
1402            );
1403        });
1404    }
1405
1406    /// A byte load's shipped guard is exact for size 1 / offset 0 (it was the
1407    /// ONE class the old ADD-computed guard already got right) and stays
1408    /// Verified under the #752 wraparound-safe shape.
1409    #[test]
1410    fn byte_load_software_bounds_guard_preserves_the_trap() {
1411        with_verification_context(|| {
1412            let validator = TranslationValidator::new();
1413            let result = validator
1414                .verify_trap_preservation(
1415                    &WasmOp::I32Load8U {
1416                        offset: 0,
1417                        align: 0,
1418                    },
1419                    &shipped_software_bounds_ops(&WasmOp::I32Load8U {
1420                        offset: 0,
1421                        align: 0,
1422                    }),
1423                )
1424                .unwrap();
1425            assert_eq!(result, ValidationResult::Verified);
1426        });
1427    }
1428
1429    /// #752 CLOSED (was the pinned `word_load_software_bounds_guard_wraps_at_
1430    /// address_top` finding): the old shape computed `addr + (offset+size-1)`
1431    /// in WRAPPING 32-bit arithmetic, so at `addr >= 0x1_0000_0000 -
1432    /// (offset+size-1)` the end address wrapped small, the BLO guard passed,
1433    /// and the access escaped below the linear-memory base — the derived gate
1434    /// exhibited the dropped-trap counterexample at `addr >= 0xFFFF_FFFD`.
1435    /// The shipped guard is now the wraparound-safe SUB-from-bound shape
1436    /// (`software_bounds_guard`): this asserts the WHOLE class Verified, i.e.
1437    /// the divergence is unsatisfiable for EVERY addr including the top of
1438    /// the address space.
1439    #[test]
1440    fn word_load_software_bounds_guard_survives_the_address_top_752() {
1441        with_verification_context(|| {
1442            let validator = TranslationValidator::new();
1443            let result = validator
1444                .verify_trap_preservation(
1445                    &WasmOp::I32Load {
1446                        offset: 0,
1447                        align: 2,
1448                    },
1449                    &shipped_software_bounds_ops(&WasmOp::I32Load {
1450                        offset: 0,
1451                        align: 2,
1452                    }),
1453                )
1454                .unwrap();
1455            assert_eq!(
1456                result,
1457                ValidationResult::Verified,
1458                "the #752 wraparound divergence must be closed for every addr"
1459            );
1460        });
1461    }
1462
1463    /// #752: every guarded access width and a non-zero static offset verify —
1464    /// the wrap escape was specifically the multi-byte / non-zero-offset
1465    /// class, so gate the whole family (loads via the shipped selector).
1466    #[test]
1467    fn all_load_widths_software_bounds_guard_verify_752() {
1468        let cases: Vec<WasmOp> = vec![
1469            WasmOp::I32Load {
1470                offset: 4,
1471                align: 2,
1472            },
1473            WasmOp::I32Load8S {
1474                offset: 3,
1475                align: 0,
1476            },
1477            WasmOp::I32Load8U {
1478                offset: 1,
1479                align: 0,
1480            },
1481            WasmOp::I32Load16S {
1482                offset: 2,
1483                align: 1,
1484            },
1485            WasmOp::I32Load16U {
1486                offset: 0,
1487                align: 1,
1488            },
1489        ];
1490        with_verification_context(|| {
1491            let validator = TranslationValidator::new();
1492            for wasm_op in &cases {
1493                let result = validator
1494                    .verify_trap_preservation(wasm_op, &shipped_software_bounds_ops(wasm_op))
1495                    .unwrap();
1496                assert_eq!(result, ValidationResult::Verified, "{wasm_op:?}");
1497            }
1498        });
1499    }
1500
1501    /// #752: the store guards use the same `software_bounds_guard` prefix —
1502    /// all three widths, with non-zero static offsets on the subword forms.
1503    #[test]
1504    fn store_software_bounds_guard_verifies_752() {
1505        let cases: Vec<WasmOp> = vec![
1506            WasmOp::I32Store {
1507                offset: 0,
1508                align: 2,
1509            },
1510            WasmOp::I32Store8 {
1511                offset: 5,
1512                align: 0,
1513            },
1514            WasmOp::I32Store16 {
1515                offset: 3,
1516                align: 1,
1517            },
1518        ];
1519        with_verification_context(|| {
1520            let validator = TranslationValidator::new();
1521            for wasm_op in &cases {
1522                let result = validator
1523                    .verify_trap_preservation(wasm_op, &shipped_software_bounds_ops(wasm_op))
1524                    .unwrap();
1525                assert_eq!(result, ValidationResult::Verified, "{wasm_op:?}");
1526            }
1527        });
1528    }
1529
1530    /// #752: a static offset past the SUBW imm12 reach takes the MOVW
1531    /// materialization arm of the guard — Verified, i.e. the register-built
1532    /// constant is derived with the same exactness as the immediate form.
1533    #[test]
1534    fn large_offset_software_bounds_guard_verifies_752() {
1535        let wasm_op = WasmOp::I32Load {
1536            offset: 0x2000,
1537            align: 2,
1538        };
1539        with_verification_context(|| {
1540            let validator = TranslationValidator::new();
1541            let result = validator
1542                .verify_trap_preservation(&wasm_op, &shipped_software_bounds_ops(&wasm_op))
1543                .unwrap();
1544            assert_eq!(result, ValidationResult::Verified);
1545        });
1546    }
1547
1548    /// #752: `offset + size > u32::MAX` can never be in bounds — the guard
1549    /// degenerates to an unconditional UDF and the gate agrees with WASM's
1550    /// always-trap on the class.
1551    #[test]
1552    fn offset_overflow_software_bounds_guard_always_traps_752() {
1553        let wasm_op = WasmOp::I32Load {
1554            offset: u32::MAX,
1555            align: 2,
1556        };
1557        with_verification_context(|| {
1558            let validator = TranslationValidator::new();
1559            let result = validator
1560                .verify_trap_preservation(&wasm_op, &shipped_software_bounds_ops(&wasm_op))
1561                .unwrap();
1562            assert_eq!(result, ValidationResult::Verified);
1563        });
1564    }
1565
1566    /// REGRESSION PIN (the #752 finding itself): the RETIRED ADD-computed
1567    /// guard shape must stay Invalid, with the counterexample at the top of
1568    /// the address space — proving the gate still catches the wraparound
1569    /// class if anything ever re-emits it.
1570    #[test]
1571    fn retired_add_computed_guard_stays_invalid_at_the_address_top_752() {
1572        use synth_synthesis::rules::{Condition, MemAddr, Operand2};
1573        with_verification_context(|| {
1574            let validator = TranslationValidator::new();
1575            let arm_ops = [
1576                ArmOp::Add {
1577                    rd: Reg::R12,
1578                    rn: Reg::R0,
1579                    op2: Operand2::Imm(3), // offset 0 + size 4 - 1
1580                },
1581                ArmOp::Cmp {
1582                    rn: Reg::R12,
1583                    op2: Operand2::Reg(Reg::R10),
1584                },
1585                ArmOp::BCondOffset {
1586                    cond: Condition::LO,
1587                    offset: 0,
1588                },
1589                ArmOp::Udf { imm: 0 },
1590                ArmOp::Ldr {
1591                    rd: Reg::R0,
1592                    addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
1593                },
1594            ];
1595            let result = validator
1596                .verify_trap_preservation(
1597                    &WasmOp::I32Load {
1598                        offset: 0,
1599                        align: 2,
1600                    },
1601                    &arm_ops,
1602                )
1603                .unwrap();
1604            match result {
1605                ValidationResult::Invalid { counterexample } => {
1606                    let addr = counterexample
1607                        .iter()
1608                        .find(|(n, _)| n == "input_0")
1609                        .map(|(_, v)| *v)
1610                        .expect("counterexample must assign the address");
1611                    assert!(
1612                        addr >= 0xFFFF_FFFD,
1613                        "the divergence is the 32-bit end-address wrap at the top \
1614                         of the address space, got addr {addr:#x}"
1615                    );
1616                }
1617                other => panic!("the retired wrapping guard must stay Invalid, got {other:?}"),
1618            }
1619        });
1620    }
1621
1622    /// The gate is satisfiable by more than one correct guard: the issue's
1623    /// reference shape (trap iff bound < k, else iff addr >u bound - k,
1624    /// k = offset+size) also verifies — the mem-OOB class gate is not pinned
1625    /// to the shipped shape.
1626    #[test]
1627    fn wraparound_safe_bounds_guard_verifies() {
1628        use synth_synthesis::rules::MemAddr;
1629        with_verification_context(|| {
1630            let validator = TranslationValidator::new();
1631            let k = 4; // offset 0, size 4
1632            let arm_ops = [
1633                // CMP R10, #k ; BHS +0 ; UDF   — bound < k ⇒ every access traps
1634                ArmOp::Cmp {
1635                    rn: Reg::R10,
1636                    op2: Operand2::Imm(k),
1637                },
1638                ArmOp::BCondOffset {
1639                    cond: Condition::HS,
1640                    offset: 0,
1641                },
1642                ArmOp::Udf { imm: 0 },
1643                // SUB R12, R10, #k ; CMP addr, R12 ; BLS +0 ; UDF — exact on
1644                // the bound >= k path (no wrap possible)
1645                ArmOp::Sub {
1646                    rd: Reg::R12,
1647                    rn: Reg::R10,
1648                    op2: Operand2::Imm(k),
1649                },
1650                ArmOp::Cmp {
1651                    rn: Reg::R0,
1652                    op2: Operand2::Reg(Reg::R12),
1653                },
1654                ArmOp::BCondOffset {
1655                    cond: Condition::LS,
1656                    offset: 0,
1657                },
1658                ArmOp::Udf { imm: 0 },
1659                ArmOp::Ldr {
1660                    rd: Reg::R0,
1661                    addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
1662                },
1663            ];
1664            let result = validator
1665                .verify_trap_preservation(
1666                    &WasmOp::I32Load {
1667                        offset: 0,
1668                        align: 2,
1669                    },
1670                    &arm_ops,
1671                )
1672                .unwrap();
1673            assert_eq!(result, ValidationResult::Verified);
1674        });
1675    }
1676
1677    // --- i32.trunc_f32_s/u (LIVE, #709 class) ---
1678
1679    /// The SHIPPED trunc domain-guard shape (instruction_selector.rs
1680    /// I32TruncF32S/U): per bound, F32Const scratch ; ordered compare into
1681    /// R0 ; CMP R0,#0 ; BNE +0 (in-range skips) ; UDF — then the VCVT.
1682    /// Operand in S0, bound scratch S1 (the gate's register convention).
1683    fn shipped_trunc_f32_guard(signed: bool) -> Vec<ArmOp> {
1684        use synth_synthesis::rules::VfpReg;
1685        let (hi, lo) = if signed {
1686            (2147483648.0_f32, -2147483648.0_f32)
1687        } else {
1688            (4294967296.0_f32, -1.0_f32)
1689        };
1690        let mut ops = Vec::new();
1691        let guard = |ops: &mut Vec<ArmOp>, bound: f32, upper: bool| {
1692            ops.push(ArmOp::F32Const {
1693                sd: VfpReg::S1,
1694                value: bound,
1695            });
1696            let cmp = if upper {
1697                ArmOp::F32Lt {
1698                    rd: Reg::R0,
1699                    sn: VfpReg::S0,
1700                    sm: VfpReg::S1,
1701                }
1702            } else if signed {
1703                ArmOp::F32Ge {
1704                    rd: Reg::R0,
1705                    sn: VfpReg::S0,
1706                    sm: VfpReg::S1,
1707                }
1708            } else {
1709                ArmOp::F32Gt {
1710                    rd: Reg::R0,
1711                    sn: VfpReg::S0,
1712                    sm: VfpReg::S1,
1713                }
1714            };
1715            ops.push(cmp);
1716            ops.push(ArmOp::Cmp {
1717                rn: Reg::R0,
1718                op2: Operand2::Imm(0),
1719            });
1720            ops.push(ArmOp::BCondOffset {
1721                cond: Condition::NE,
1722                offset: 0,
1723            });
1724            ops.push(ArmOp::Udf { imm: 0 });
1725        };
1726        guard(&mut ops, hi, true);
1727        guard(&mut ops, lo, false);
1728        if signed {
1729            ops.push(ArmOp::I32TruncF32S {
1730                rd: Reg::R0,
1731                sm: VfpReg::S0,
1732            });
1733        } else {
1734            ops.push(ArmOp::I32TruncF32U {
1735                rd: Reg::R0,
1736                sm: VfpReg::S0,
1737            });
1738        }
1739        ops
1740    }
1741
1742    #[test]
1743    fn trunc_f32_s_domain_guard_preserves_the_trap() {
1744        with_verification_context(|| {
1745            let validator = TranslationValidator::new();
1746            let result = validator
1747                .verify_trap_preservation(&WasmOp::I32TruncF32S, &shipped_trunc_f32_guard(true))
1748                .unwrap();
1749            assert_eq!(result, ValidationResult::Verified);
1750        });
1751    }
1752
1753    #[test]
1754    fn trunc_f32_u_domain_guard_preserves_the_trap() {
1755        with_verification_context(|| {
1756            let validator = TranslationValidator::new();
1757            let result = validator
1758                .verify_trap_preservation(&WasmOp::I32TruncF32U, &shipped_trunc_f32_guard(false))
1759                .unwrap();
1760            assert_eq!(result, ValidationResult::Verified);
1761        });
1762    }
1763
1764    /// RED-FIRST for the #709 class: the bare saturating VCVT (guards
1765    /// stripped) never traps — rejected with a counterexample.
1766    #[test]
1767    fn trunc_f32_without_domain_guard_is_rejected() {
1768        use synth_synthesis::rules::VfpReg;
1769        with_verification_context(|| {
1770            let validator = TranslationValidator::new();
1771            let arm_ops = [ArmOp::I32TruncF32S {
1772                rd: Reg::R0,
1773                sm: VfpReg::S0,
1774            }];
1775            let result = validator
1776                .verify_trap_preservation(&WasmOp::I32TruncF32S, &arm_ops)
1777                .unwrap();
1778            assert!(
1779                matches!(result, ValidationResult::Invalid { .. }),
1780                "guard-stripped trunc must be Invalid, got {result:?}"
1781            );
1782        });
1783    }
1784
1785    /// Half a domain guard (upper bound only) drops the lower-bound trap.
1786    #[test]
1787    fn trunc_f32_with_only_upper_guard_is_rejected() {
1788        with_verification_context(|| {
1789            let validator = TranslationValidator::new();
1790            let mut arm_ops = shipped_trunc_f32_guard(true);
1791            // Strip the second (lower-bound) guard: 5 ops per guard block.
1792            arm_ops.drain(5..10);
1793            let result = validator
1794                .verify_trap_preservation(&WasmOp::I32TruncF32S, &arm_ops)
1795                .unwrap();
1796            assert!(
1797                matches!(result, ValidationResult::Invalid { .. }),
1798                "upper-only trunc guard must be Invalid, got {result:?}"
1799            );
1800        });
1801    }
1802
1803    // --- i32.trunc_f64_s/u (LIVE, #709/#756 class, D-register guard) ---
1804
1805    /// The SHIPPED f64 trunc domain-guard shape (instruction_selector.rs
1806    /// I32TruncF64S/U, lines 3158-3220): per bound, F64Const scratch into D1 ;
1807    /// ordered VCMP.F64 into R0 ; CMP R0,#0 ; BNE +0 (in-range skips) ; UDF —
1808    /// then the VCVT. Operand in D0, bound scratch D1. Upper bound uses F64Lt,
1809    /// lower uses F64Gt (STRICT — both false on NaN, so NaN traps at the first
1810    /// guard). Bounds are the exact selector constants.
1811    fn shipped_trunc_f64_guard(signed: bool) -> Vec<ArmOp> {
1812        use synth_synthesis::rules::VfpReg;
1813        let (hi, lo) = if signed {
1814            (2147483648.0_f64, -2147483649.0_f64) // 2^31, -(2^31)-1
1815        } else {
1816            (4294967296.0_f64, -1.0_f64) // 2^32, -1.0
1817        };
1818        let mut ops = Vec::new();
1819        let guard = |ops: &mut Vec<ArmOp>, bound: f64, upper: bool| {
1820            ops.push(ArmOp::F64Const {
1821                dd: VfpReg::D1,
1822                value: bound,
1823            });
1824            let cmp = if upper {
1825                ArmOp::F64Lt {
1826                    rd: Reg::R0,
1827                    dn: VfpReg::D0,
1828                    dm: VfpReg::D1,
1829                }
1830            } else {
1831                ArmOp::F64Gt {
1832                    rd: Reg::R0,
1833                    dn: VfpReg::D0,
1834                    dm: VfpReg::D1,
1835                }
1836            };
1837            ops.push(cmp);
1838            ops.push(ArmOp::Cmp {
1839                rn: Reg::R0,
1840                op2: Operand2::Imm(0),
1841            });
1842            ops.push(ArmOp::BCondOffset {
1843                cond: Condition::NE,
1844                offset: 0,
1845            });
1846            ops.push(ArmOp::Udf { imm: 0 });
1847        };
1848        guard(&mut ops, hi, true); // x < hi (also traps NaN)
1849        guard(&mut ops, lo, false); // x > lo
1850        if signed {
1851            ops.push(ArmOp::I32TruncF64S {
1852                rd: Reg::R0,
1853                dm: VfpReg::D0,
1854            });
1855        } else {
1856            ops.push(ArmOp::I32TruncF64U {
1857                rd: Reg::R0,
1858                dm: VfpReg::D0,
1859            });
1860        }
1861        ops
1862    }
1863
1864    #[test]
1865    fn trunc_f64_s_domain_guard_preserves_the_trap() {
1866        with_verification_context(|| {
1867            let validator = TranslationValidator::new();
1868            let result = validator
1869                .verify_trap_preservation(&WasmOp::I32TruncF64S, &shipped_trunc_f64_guard(true))
1870                .unwrap();
1871            assert_eq!(
1872                result,
1873                ValidationResult::Verified,
1874                "GREEN: correct f64→i32_s domain guard must be Verified (Unsat)"
1875            );
1876        });
1877    }
1878
1879    #[test]
1880    fn trunc_f64_u_domain_guard_preserves_the_trap() {
1881        with_verification_context(|| {
1882            let validator = TranslationValidator::new();
1883            let result = validator
1884                .verify_trap_preservation(&WasmOp::I32TruncF64U, &shipped_trunc_f64_guard(false))
1885                .unwrap();
1886            assert_eq!(
1887                result,
1888                ValidationResult::Verified,
1889                "GREEN: correct f64→i32_u domain guard must be Verified (Unsat)"
1890            );
1891        });
1892    }
1893
1894    /// RED-FIRST for the f64 #709 class: the bare saturating VCVT (guards
1895    /// stripped) never traps — rejected with a counterexample (Sat).
1896    #[test]
1897    fn trunc_f64_without_domain_guard_is_rejected() {
1898        use synth_synthesis::rules::VfpReg;
1899        with_verification_context(|| {
1900            let validator = TranslationValidator::new();
1901            let arm_ops = [ArmOp::I32TruncF64S {
1902                rd: Reg::R0,
1903                dm: VfpReg::D0,
1904            }];
1905            let result = validator
1906                .verify_trap_preservation(&WasmOp::I32TruncF64S, &arm_ops)
1907                .unwrap();
1908            assert!(
1909                matches!(result, ValidationResult::Invalid { .. }),
1910                "RED: guard-stripped f64 trunc must be Invalid (Sat), got {result:?}"
1911            );
1912        });
1913    }
1914
1915    /// RED-FIRST: half an f64 domain guard (upper bound only) drops the
1916    /// lower-bound (NaN-negative-overflow) trap — must be caught.
1917    #[test]
1918    fn trunc_f64_with_only_upper_guard_is_rejected() {
1919        with_verification_context(|| {
1920            let validator = TranslationValidator::new();
1921            let mut arm_ops = shipped_trunc_f64_guard(true);
1922            // Strip the second (lower-bound) guard: 5 ops per guard block.
1923            arm_ops.drain(5..10);
1924            let result = validator
1925                .verify_trap_preservation(&WasmOp::I32TruncF64S, &arm_ops)
1926                .unwrap();
1927            assert!(
1928                matches!(result, ValidationResult::Invalid { .. }),
1929                "RED: upper-only f64 trunc guard must be Invalid (Sat), got {result:?}"
1930            );
1931        });
1932    }
1933
1934    // --- i64 div/rem (LIVE at the pseudo-op guard-field level, #756) ---
1935
1936    /// The SHIPPED i64 div/rem pseudo-op (instruction_selector.rs 5556-5604):
1937    /// register pairs R0:R1 (dividend) / R2:R3 (divisor), result R0:R1, with
1938    /// both guard-elision fields `false` (full guards). `elide_zero`/
1939    /// `elide_overflow` override the fields to model a dropped guard.
1940    fn shipped_i64_div_rem(op: &WasmOp, elide_zero: bool, elide_overflow: bool) -> Vec<ArmOp> {
1941        let arm = match op {
1942            WasmOp::I64DivS => ArmOp::I64DivS {
1943                rdlo: Reg::R0,
1944                rdhi: Reg::R1,
1945                rnlo: Reg::R0,
1946                rnhi: Reg::R1,
1947                rmlo: Reg::R2,
1948                rmhi: Reg::R3,
1949                elide_zero_guard: elide_zero,
1950                elide_overflow_guard: elide_overflow,
1951            },
1952            WasmOp::I64DivU => ArmOp::I64DivU {
1953                rdlo: Reg::R0,
1954                rdhi: Reg::R1,
1955                rnlo: Reg::R0,
1956                rnhi: Reg::R1,
1957                rmlo: Reg::R2,
1958                rmhi: Reg::R3,
1959                elide_zero_guard: elide_zero,
1960            },
1961            WasmOp::I64RemS => ArmOp::I64RemS {
1962                rdlo: Reg::R0,
1963                rdhi: Reg::R1,
1964                rnlo: Reg::R0,
1965                rnhi: Reg::R1,
1966                rmlo: Reg::R2,
1967                rmhi: Reg::R3,
1968                elide_zero_guard: elide_zero,
1969            },
1970            WasmOp::I64RemU => ArmOp::I64RemU {
1971                rdlo: Reg::R0,
1972                rdhi: Reg::R1,
1973                rnlo: Reg::R0,
1974                rnhi: Reg::R1,
1975                rmlo: Reg::R2,
1976                rmhi: Reg::R3,
1977                elide_zero_guard: elide_zero,
1978            },
1979            _ => unreachable!("shipped_i64_div_rem: not an i64 div/rem op"),
1980        };
1981        vec![arm]
1982    }
1983
1984    #[test]
1985    fn i64_div_rem_all_four_full_guards_preserve_the_trap() {
1986        with_verification_context(|| {
1987            let validator = TranslationValidator::new();
1988            for op in [
1989                WasmOp::I64DivU,
1990                WasmOp::I64DivS,
1991                WasmOp::I64RemU,
1992                WasmOp::I64RemS,
1993            ] {
1994                let arm_ops = shipped_i64_div_rem(&op, false, false);
1995                let result = validator.verify_trap_preservation(&op, &arm_ops).unwrap();
1996                assert_eq!(
1997                    result,
1998                    ValidationResult::Verified,
1999                    "GREEN: {op:?} with full guards must be Verified (Unsat)"
2000                );
2001            }
2002        });
2003    }
2004
2005    /// RED-FIRST: dropping the ÷0 guard (elide_zero_guard = true) on ANY of the
2006    /// four i64 div/rem ops must be caught — the shipped fields say the guard
2007    /// was elided without a discharged divisor-nonzero fact.
2008    #[test]
2009    fn i64_div_rem_dropped_zero_guard_is_rejected() {
2010        with_verification_context(|| {
2011            let validator = TranslationValidator::new();
2012            for op in [
2013                WasmOp::I64DivU,
2014                WasmOp::I64DivS,
2015                WasmOp::I64RemU,
2016                WasmOp::I64RemS,
2017            ] {
2018                let arm_ops = shipped_i64_div_rem(&op, true, false);
2019                let result = validator.verify_trap_preservation(&op, &arm_ops).unwrap();
2020                assert!(
2021                    matches!(result, ValidationResult::Invalid { .. }),
2022                    "RED: {op:?} with the ÷0 guard dropped must be Invalid (Sat), got {result:?}"
2023                );
2024            }
2025        });
2026    }
2027
2028    /// RED-FIRST: dropping ONLY the INT64_MIN/-1 overflow guard on div_s (÷0
2029    /// still present) is a partial #633-class drop — must be caught.
2030    #[test]
2031    fn i64_div_s_dropped_overflow_guard_is_rejected() {
2032        with_verification_context(|| {
2033            let validator = TranslationValidator::new();
2034            let arm_ops = shipped_i64_div_rem(&WasmOp::I64DivS, false, true);
2035            let result = validator
2036                .verify_trap_preservation(&WasmOp::I64DivS, &arm_ops)
2037                .unwrap();
2038            assert!(
2039                matches!(result, ValidationResult::Invalid { .. }),
2040                "RED: i64.div_s with the overflow guard dropped must be Invalid (Sat), got {result:?}"
2041            );
2042        });
2043    }
2044
2045    /// RED-FIRST: dropping BOTH div_s guards is the fully-unguarded shape.
2046    #[test]
2047    fn i64_div_s_dropped_both_guards_is_rejected() {
2048        with_verification_context(|| {
2049            let validator = TranslationValidator::new();
2050            let arm_ops = shipped_i64_div_rem(&WasmOp::I64DivS, true, true);
2051            let result = validator
2052                .verify_trap_preservation(&WasmOp::I64DivS, &arm_ops)
2053                .unwrap();
2054            assert!(
2055                matches!(result, ValidationResult::Invalid { .. }),
2056                "RED: i64.div_s with both guards dropped must be Invalid (Sat), got {result:?}"
2057            );
2058        });
2059    }
2060
2061    /// NON-VACUITY control for the div_s overflow clause: the OVERFLOW-only
2062    /// drop and the correct full guard must give OPPOSITE verdicts. Proves the
2063    /// gate discriminates on the overflow field, not just ÷0.
2064    #[test]
2065    fn i64_div_s_overflow_field_is_load_bearing() {
2066        with_verification_context(|| {
2067            let validator = TranslationValidator::new();
2068            let full = validator
2069                .verify_trap_preservation(
2070                    &WasmOp::I64DivS,
2071                    &shipped_i64_div_rem(&WasmOp::I64DivS, false, false),
2072                )
2073                .unwrap();
2074            let overflow_dropped = validator
2075                .verify_trap_preservation(
2076                    &WasmOp::I64DivS,
2077                    &shipped_i64_div_rem(&WasmOp::I64DivS, false, true),
2078                )
2079                .unwrap();
2080            assert_eq!(full, ValidationResult::Verified);
2081            assert!(matches!(overflow_dropped, ValidationResult::Invalid { .. }));
2082            assert_ne!(
2083                full, overflow_dropped,
2084                "non-vacuity: the overflow-guard field must change the verdict"
2085            );
2086        });
2087    }
2088
2089    /// NON-VACUITY dump (run with `--nocapture`): prints the raw
2090    /// Preserved/Dropped verdict for the shipped-vs-dropped-guard shapes of the
2091    /// two newly-live classes, so the gate's discrimination is visible, not
2092    /// merely asserted. A gate that printed the SAME verdict on both rows would
2093    /// be vacuous.
2094    #[test]
2095    fn dump_756_non_vacuity_verdicts() {
2096        with_verification_context(|| {
2097            let validator = TranslationValidator::new();
2098            let raw = |r: &ValidationResult| match r {
2099                ValidationResult::Verified => "Verified/Unsat (trap PRESERVED)",
2100                ValidationResult::Invalid { .. } => "Invalid/Sat  (trap DROPPED — caught)",
2101                ValidationResult::Unknown { .. } => "Unknown",
2102            };
2103            println!("\n=== #756 live trap-preservation non-vacuity ===");
2104            for (op, oflow_field) in [
2105                (WasmOp::I64DivU, false),
2106                (WasmOp::I64DivS, true),
2107                (WasmOp::I64RemU, false),
2108                (WasmOp::I64RemS, false),
2109            ] {
2110                let green = validator
2111                    .verify_trap_preservation(&op, &shipped_i64_div_rem(&op, false, false))
2112                    .unwrap();
2113                let red = validator
2114                    .verify_trap_preservation(&op, &shipped_i64_div_rem(&op, true, false))
2115                    .unwrap();
2116                println!(
2117                    "  {op:?}: full-guards -> {} | drop-÷0 -> {}",
2118                    raw(&green),
2119                    raw(&red)
2120                );
2121                assert_ne!(
2122                    green, red,
2123                    "{op:?}: green and red must differ (non-vacuous)"
2124                );
2125                if oflow_field {
2126                    let red_o = validator
2127                        .verify_trap_preservation(&op, &shipped_i64_div_rem(&op, false, true))
2128                        .unwrap();
2129                    println!("  {op:?}: drop-overflow -> {}", raw(&red_o));
2130                    assert_ne!(green, red_o);
2131                }
2132            }
2133            for (op, sgn) in [(WasmOp::I32TruncF64S, true), (WasmOp::I32TruncF64U, false)] {
2134                let green = validator
2135                    .verify_trap_preservation(&op, &shipped_trunc_f64_guard(sgn))
2136                    .unwrap();
2137                let bare = if sgn {
2138                    vec![ArmOp::I32TruncF64S {
2139                        rd: Reg::R0,
2140                        dm: synth_synthesis::rules::VfpReg::D0,
2141                    }]
2142                } else {
2143                    vec![ArmOp::I32TruncF64U {
2144                        rd: Reg::R0,
2145                        dm: synth_synthesis::rules::VfpReg::D0,
2146                    }]
2147                };
2148                let red = validator.verify_trap_preservation(&op, &bare).unwrap();
2149                println!(
2150                    "  {op:?}: domain-guard -> {} | bare-VCVT -> {}",
2151                    raw(&green),
2152                    raw(&red)
2153                );
2154                assert_ne!(
2155                    green, red,
2156                    "{op:?}: green and red must differ (non-vacuous)"
2157                );
2158            }
2159            println!("=== all rows discriminate: gate is non-vacuous ===\n");
2160        });
2161    }
2162
2163    // --- i64 rem VALUE model (VCR-VER, #825/#836): native BvTerm::Urem ---
2164    //
2165    // The i64 rem_u/rem_s VC now asserts the VALUE, not just the trap. Under
2166    // the old HAVOC model the register result was an unconstrained fresh
2167    // symbol, so ANY lowering (correct, wrong-remainder, wrong-destination)
2168    // trivially satisfied a value-only obligation — the model proved nothing.
2169    // These tests pin the value model as load-bearing.
2170
2171    /// Build an i64 rem pseudo-op with a chosen DESTINATION register pair. The
2172    /// shipped ABI is `rd = R0:R1`; a lowering that writes elsewhere leaves
2173    /// R0:R1 unrelated to the remainder and must be REJECTED by the value VC.
2174    fn i64_rem_with_dest(op: &WasmOp, rdlo: Reg, rdhi: Reg) -> Vec<ArmOp> {
2175        let arm = match op {
2176            WasmOp::I64RemU => ArmOp::I64RemU {
2177                rdlo,
2178                rdhi,
2179                rnlo: Reg::R0,
2180                rnhi: Reg::R1,
2181                rmlo: Reg::R2,
2182                rmhi: Reg::R3,
2183                elide_zero_guard: false,
2184            },
2185            WasmOp::I64RemS => ArmOp::I64RemS {
2186                rdlo,
2187                rdhi,
2188                rnlo: Reg::R0,
2189                rnhi: Reg::R1,
2190                rmlo: Reg::R2,
2191                rmhi: Reg::R3,
2192                elide_zero_guard: false,
2193            },
2194            _ => unreachable!("i64_rem_with_dest: not an i64 rem op"),
2195        };
2196        vec![arm]
2197    }
2198
2199    /// GREEN: the shipped rem_u/rem_s lowering (`rd = R0:R1`) computes the
2200    /// correct 64-bit remainder into the ABI return pair — the value+trap VC
2201    /// must be Verified.
2202    #[test]
2203    fn i64_rem_shipped_value_is_verified() {
2204        with_verification_context(|| {
2205            let validator = TranslationValidator::new();
2206            for op in [WasmOp::I64RemU, WasmOp::I64RemS] {
2207                let arm_ops = shipped_i64_div_rem(&op, false, false);
2208                let result = validator.verify_trap_preservation(&op, &arm_ops).unwrap();
2209                assert_eq!(
2210                    result,
2211                    ValidationResult::Verified,
2212                    "GREEN: {op:?} shipped lowering (correct value + ÷0 guard) must be Verified"
2213                );
2214            }
2215        });
2216    }
2217
2218    /// RED / NON-VACUITY (the key gate): a lowering that writes the remainder
2219    /// to the WRONG destination pair (`rd = R2:R3` instead of the ABI `R0:R1`)
2220    /// leaves R0:R1 holding the (unrelated) input operands, so the ABI return
2221    /// pair is NOT the remainder. Under the old HAVOC model this was ACCEPTED
2222    /// (the result symbol was unconstrained); the native `BvTerm::Urem` value
2223    /// model now REJECTS it. This is the accepted-under-havoc → rejected-now
2224    /// discriminator.
2225    #[test]
2226    fn i64_rem_wrong_destination_register_is_rejected() {
2227        with_verification_context(|| {
2228            let validator = TranslationValidator::new();
2229            for op in [WasmOp::I64RemU, WasmOp::I64RemS] {
2230                let arm_ops = i64_rem_with_dest(&op, Reg::R2, Reg::R3);
2231                let result = validator.verify_trap_preservation(&op, &arm_ops).unwrap();
2232                assert!(
2233                    matches!(result, ValidationResult::Invalid { .. }),
2234                    "RED: {op:?} writing the remainder to R2:R3 (not the ABI R0:R1) \
2235                     leaves R0:R1 non-remainder — must be Invalid, got {result:?}"
2236                );
2237            }
2238        });
2239    }
2240
2241    /// RED / NON-VACUITY on the ARITHMETIC, not just the register file: a
2242    /// lowering that computes the remainder with the WRONG SIGNEDNESS —
2243    /// `rem_u` lowered to the signed pseudo-op, or `rem_s` to the unsigned one
2244    /// — writes to the correct ABI pair `R0:R1` and preserves the ÷0 trap, so
2245    /// the destination check above cannot see it. Only a real value model can:
2246    /// `bvurem` and `bvsrem` differ exactly when an operand's sign bit is set.
2247    ///
2248    /// This is the strictly stronger discriminator. It fails under HAVOC (both
2249    /// sides unconstrained ⟹ Verified) and under any model that got the value
2250    /// "shape" right but the operation wrong, and the counterexamples it
2251    /// produces are the expected ones (a negative dividend for `rem_u`-as-
2252    /// `rem_s`, a negative divisor for `rem_s`-as-`rem_u`).
2253    #[test]
2254    fn i64_rem_wrong_signedness_is_rejected() {
2255        with_verification_context(|| {
2256            let validator = TranslationValidator::new();
2257            // (spec op, lowering op) — same ABI destination, wrong arithmetic.
2258            for (spec, lowered) in [
2259                (WasmOp::I64RemU, WasmOp::I64RemS),
2260                (WasmOp::I64RemS, WasmOp::I64RemU),
2261            ] {
2262                let arm_ops = i64_rem_with_dest(&lowered, Reg::R0, Reg::R1);
2263                let result = validator.verify_trap_preservation(&spec, &arm_ops).unwrap();
2264                assert!(
2265                    matches!(result, ValidationResult::Invalid { .. }),
2266                    "RED: {spec:?} lowered as {lowered:?} computes the wrong remainder \
2267                     into the right registers — must be Invalid, got {result:?}"
2268                );
2269            }
2270        });
2271    }
2272
2273    /// NON-VACUITY control: the correct-destination and wrong-destination
2274    /// lowerings must give OPPOSITE verdicts. A gate that returned the same
2275    /// verdict for both (as the HAVOC model did — both trivially valued) would
2276    /// be vacuous.
2277    #[test]
2278    fn i64_rem_destination_field_is_load_bearing() {
2279        with_verification_context(|| {
2280            let validator = TranslationValidator::new();
2281            for op in [WasmOp::I64RemU, WasmOp::I64RemS] {
2282                let correct = validator
2283                    .verify_trap_preservation(&op, &i64_rem_with_dest(&op, Reg::R0, Reg::R1))
2284                    .unwrap();
2285                let wrong = validator
2286                    .verify_trap_preservation(&op, &i64_rem_with_dest(&op, Reg::R2, Reg::R3))
2287                    .unwrap();
2288                assert_eq!(correct, ValidationResult::Verified, "{op:?} correct dest");
2289                assert!(
2290                    matches!(wrong, ValidationResult::Invalid { .. }),
2291                    "{op:?} wrong dest"
2292                );
2293                assert_ne!(
2294                    correct, wrong,
2295                    "non-vacuity: {op:?} destination register must change the verdict"
2296                );
2297            }
2298        });
2299    }
2300
2301    /// EVIDENCE that the value model closed a real gap: the OLD trap-only VC
2302    /// (`verify_i64_div_rem_trap_preservation`, still present for div) ACCEPTS
2303    /// the wrong-destination rem lowering — it never reads the value — whereas
2304    /// the NEW value VC (`verify_i64_rem_value_preservation`, dispatched by
2305    /// `verify_trap_preservation`) REJECTS it. Opposite verdicts from the two
2306    /// gates on the same input is the accepted-under-havoc → rejected-now
2307    /// proof, made concrete rather than merely narrated.
2308    #[test]
2309    fn i64_rem_value_model_closes_a_trap_only_gap() {
2310        with_verification_context(|| {
2311            let validator = TranslationValidator::new();
2312            for op in [WasmOp::I64RemU, WasmOp::I64RemS] {
2313                let wrong = i64_rem_with_dest(&op, Reg::R2, Reg::R3);
2314                // Old trap-only path: value/destination invisible → accepted.
2315                let trap_only = validator
2316                    .verify_i64_div_rem_trap_preservation(&op, &wrong)
2317                    .unwrap();
2318                assert_eq!(
2319                    trap_only,
2320                    ValidationResult::Verified,
2321                    "the trap-ONLY VC is blind to the wrong destination (this is the \
2322                     havoc-era gap): {op:?} expected Verified, got {trap_only:?}"
2323                );
2324                // New value+trap path: destination is load-bearing → rejected.
2325                let value = validator.verify_trap_preservation(&op, &wrong).unwrap();
2326                assert!(
2327                    matches!(value, ValidationResult::Invalid { .. }),
2328                    "the value VC catches it: {op:?} expected Invalid, got {value:?}"
2329                );
2330                assert_ne!(trap_only, value, "the two gates must disagree ({op:?})");
2331            }
2332        });
2333    }
2334
2335    // --- call_indirect (LIVE at the pseudo-op guard level, #642/#664/#676) ---
2336
2337    fn call_indirect_pseudo(
2338        table_size: u32,
2339        null_check: bool,
2340        type_check: Option<(u32, u32)>,
2341    ) -> ArmOp {
2342        ArmOp::CallIndirect {
2343            rd: Reg::R0,
2344            type_idx: 0,
2345            table_index_reg: Reg::R0,
2346            table_size,
2347            table_byte_offset: 0,
2348            null_check,
2349            type_check,
2350        }
2351    }
2352
2353    #[test]
2354    fn call_indirect_matching_guards_preserve_the_traps() {
2355        with_verification_context(|| {
2356            let validator = TranslationValidator::new();
2357            // Homogeneous table, all slots initialized: bounds clause only.
2358            let result = validator
2359                .verify_call_indirect_trap_preservation(
2360                    &call_indirect_pseudo(8, false, None),
2361                    &CallIndirectSpec {
2362                        table_size: 8,
2363                        may_have_null_slot: false,
2364                        heterogeneous_expected_type: None,
2365                    },
2366                )
2367                .unwrap();
2368            assert_eq!(result, ValidationResult::Verified);
2369            // Null-slot table + runtime type check, guards resolved.
2370            let result = validator
2371                .verify_call_indirect_trap_preservation(
2372                    &call_indirect_pseudo(8, true, Some((3, 32))),
2373                    &CallIndirectSpec {
2374                        table_size: 8,
2375                        may_have_null_slot: true,
2376                        heterogeneous_expected_type: Some(3),
2377                    },
2378                )
2379                .unwrap();
2380            assert_eq!(result, ValidationResult::Verified);
2381        });
2382    }
2383
2384    /// RED-FIRST for the #664 class: table has uninitialized slots but the
2385    /// selector resolved `null_check: false` — the null trap is dropped.
2386    #[test]
2387    fn call_indirect_dropped_null_check_is_rejected() {
2388        with_verification_context(|| {
2389            let validator = TranslationValidator::new();
2390            let result = validator
2391                .verify_call_indirect_trap_preservation(
2392                    &call_indirect_pseudo(8, false, None),
2393                    &CallIndirectSpec {
2394                        table_size: 8,
2395                        may_have_null_slot: true,
2396                        heterogeneous_expected_type: None,
2397                    },
2398                )
2399                .unwrap();
2400            assert!(
2401                matches!(result, ValidationResult::Invalid { .. }),
2402                "dropped null check must be Invalid, got {result:?}"
2403            );
2404        });
2405    }
2406
2407    /// RED-FIRST for the #642 class: the selector resolved the WRONG table
2408    /// size — indices in the gap escape the bounds trap.
2409    #[test]
2410    fn call_indirect_wrong_table_size_is_rejected() {
2411        with_verification_context(|| {
2412            let validator = TranslationValidator::new();
2413            let result = validator
2414                .verify_call_indirect_trap_preservation(
2415                    &call_indirect_pseudo(16, false, None),
2416                    &CallIndirectSpec {
2417                        table_size: 8,
2418                        may_have_null_slot: false,
2419                        heterogeneous_expected_type: None,
2420                    },
2421                )
2422                .unwrap();
2423            assert!(
2424                matches!(result, ValidationResult::Invalid { .. }),
2425                "wrong bounds size must be Invalid, got {result:?}"
2426            );
2427        });
2428    }
2429
2430    /// RED-FIRST for the #676 class: heterogeneous table but the runtime
2431    /// type check was dropped.
2432    #[test]
2433    fn call_indirect_dropped_type_check_is_rejected() {
2434        with_verification_context(|| {
2435            let validator = TranslationValidator::new();
2436            let result = validator
2437                .verify_call_indirect_trap_preservation(
2438                    &call_indirect_pseudo(8, true, None),
2439                    &CallIndirectSpec {
2440                        table_size: 8,
2441                        may_have_null_slot: true,
2442                        heterogeneous_expected_type: Some(3),
2443                    },
2444                )
2445                .unwrap();
2446            assert!(
2447                matches!(result, ValidationResult::Invalid { .. }),
2448                "dropped type check must be Invalid, got {result:?}"
2449            );
2450        });
2451    }
2452
2453    // --- verify_rule routes partial ops through the trap VC (mandatory) ---
2454
2455    #[test]
2456    fn verify_rule_routes_partial_ops_through_the_trap_gate() {
2457        with_verification_context(|| {
2458            let validator = TranslationValidator::new();
2459            // A bare-UDIV rule: value-plausible, trap-dropping. verify_rule
2460            // must report Invalid (via the trap VC), not silently value-check.
2461            let rule = SynthesisRule {
2462                name: "i32.div_u → bare UDIV (trap-dropping)".into(),
2463                priority: 0,
2464                pattern: Pattern::WasmInstr(WasmOp::I32DivU),
2465                replacement: Replacement::ArmInstr(ArmOp::Udiv {
2466                    rd: Reg::R0,
2467                    rn: Reg::R0,
2468                    rm: Reg::R1,
2469                }),
2470                cost: Cost {
2471                    cycles: 1,
2472                    code_size: 4,
2473                    registers: 2,
2474                },
2475            };
2476            let result = validator.verify_rule(&rule).unwrap();
2477            assert!(
2478                matches!(result, ValidationResult::Invalid { .. }),
2479                "verify_rule must reject the trap-dropping div rule, got {result:?}"
2480            );
2481
2482            // The guarded shape goes green through the same entry point.
2483            let rule = SynthesisRule {
2484                name: "i32.div_u → guarded UDIV".into(),
2485                priority: 0,
2486                pattern: Pattern::WasmInstr(WasmOp::I32DivU),
2487                replacement: Replacement::ArmSequence(shipped_divu_guard()),
2488                cost: Cost {
2489                    cycles: 4,
2490                    code_size: 10,
2491                    registers: 2,
2492                },
2493            };
2494            assert_eq!(
2495                validator.verify_rule(&rule).unwrap(),
2496                ValidationResult::Verified
2497            );
2498        });
2499    }
2500
2501    #[test]
2502    fn trap_preservation_gate_rejects_non_div_ops() {
2503        with_verification_context(|| {
2504            let validator = TranslationValidator::new();
2505            let err = validator
2506                .verify_div_rem_trap_preservation(&WasmOp::I32Add, &[])
2507                .unwrap_err();
2508            assert!(matches!(err, VerificationError::UnsupportedOperation(_)));
2509            // i64 div/rem is a div op but this method models 32-bit only —
2510            // it must Err rather than build wrong-width terms.
2511            let err64 = validator
2512                .verify_div_rem_trap_preservation(&WasmOp::I64DivU, &[])
2513                .unwrap_err();
2514            assert!(matches!(err64, VerificationError::UnsupportedOperation(_)));
2515        });
2516    }
2517
2518    #[test]
2519    fn test_verify_add_correct() {
2520        with_verification_context(|| {
2521            let validator = TranslationValidator::new();
2522
2523            let rule = create_test_rule(
2524                WasmOp::I32Add,
2525                ArmOp::Add {
2526                    rd: Reg::R0,
2527                    rn: Reg::R0,
2528                    op2: Operand2::Reg(Reg::R1),
2529                },
2530            );
2531
2532            let result = validator.verify_rule(&rule).unwrap();
2533            assert_eq!(result, ValidationResult::Verified);
2534        });
2535    }
2536
2537    #[test]
2538    fn test_verify_sub_correct() {
2539        with_verification_context(|| {
2540            let validator = TranslationValidator::new();
2541
2542            let rule = create_test_rule(
2543                WasmOp::I32Sub,
2544                ArmOp::Sub {
2545                    rd: Reg::R0,
2546                    rn: Reg::R0,
2547                    op2: Operand2::Reg(Reg::R1),
2548                },
2549            );
2550
2551            let result = validator.verify_rule(&rule).unwrap();
2552            assert_eq!(result, ValidationResult::Verified);
2553        });
2554    }
2555
2556    #[test]
2557    fn test_verify_mul_correct() {
2558        with_verification_context(|| {
2559            let validator = TranslationValidator::new();
2560
2561            let rule = create_test_rule(
2562                WasmOp::I32Mul,
2563                ArmOp::Mul {
2564                    rd: Reg::R0,
2565                    rn: Reg::R0,
2566                    rm: Reg::R1,
2567                },
2568            );
2569
2570            let result = validator.verify_rule(&rule).unwrap();
2571            assert_eq!(result, ValidationResult::Verified);
2572        });
2573    }
2574
2575    #[test]
2576    fn test_verify_and_correct() {
2577        with_verification_context(|| {
2578            let validator = TranslationValidator::new();
2579
2580            let rule = create_test_rule(
2581                WasmOp::I32And,
2582                ArmOp::And {
2583                    rd: Reg::R0,
2584                    rn: Reg::R0,
2585                    op2: Operand2::Reg(Reg::R1),
2586                },
2587            );
2588
2589            let result = validator.verify_rule(&rule).unwrap();
2590            assert_eq!(result, ValidationResult::Verified);
2591        });
2592    }
2593
2594    #[test]
2595    fn test_verify_incorrect_rule() {
2596        with_verification_context(|| {
2597            let validator = TranslationValidator::new();
2598
2599            // INCORRECT rule: WASM i32.add -> ARM SUB (should find counterexample)
2600            let rule = create_test_rule(
2601                WasmOp::I32Add,
2602                ArmOp::Sub {
2603                    rd: Reg::R0,
2604                    rn: Reg::R0,
2605                    op2: Operand2::Reg(Reg::R1),
2606                },
2607            );
2608
2609            let result = validator.verify_rule(&rule).unwrap();
2610
2611            match result {
2612                ValidationResult::Invalid { counterexample } => {
2613                    assert!(!counterexample.is_empty());
2614                }
2615                _ => panic!("Expected counterexample but got: {:?}", result),
2616            }
2617        });
2618    }
2619
2620    #[test]
2621    fn test_verify_bitwise_ops() {
2622        with_verification_context(|| {
2623            let validator = TranslationValidator::new();
2624
2625            // Test OR
2626            let or_rule = create_test_rule(
2627                WasmOp::I32Or,
2628                ArmOp::Orr {
2629                    rd: Reg::R0,
2630                    rn: Reg::R0,
2631                    op2: Operand2::Reg(Reg::R1),
2632                },
2633            );
2634            assert_eq!(
2635                validator.verify_rule(&or_rule).unwrap(),
2636                ValidationResult::Verified
2637            );
2638
2639            // Test XOR
2640            let xor_rule = create_test_rule(
2641                WasmOp::I32Xor,
2642                ArmOp::Eor {
2643                    rd: Reg::R0,
2644                    rn: Reg::R0,
2645                    op2: Operand2::Reg(Reg::R1),
2646                },
2647            );
2648            assert_eq!(
2649                validator.verify_rule(&xor_rule).unwrap(),
2650                ValidationResult::Verified
2651            );
2652        });
2653    }
2654
2655    #[test]
2656    fn test_verify_shift_ops() {
2657        // Note: Shift operations require concrete immediate values in ARM
2658        // but use register operands in WASM. Verification requires
2659        // modeling the shift amount modulo operation.
2660        // TODO: Implement shift verification with proper modulo handling
2661    }
2662}