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