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