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                | WasmOp::Unreachable
161                | WasmOp::I32Load { .. }
162                | WasmOp::I32Load8S { .. }
163                | WasmOp::I32Load8U { .. }
164                | WasmOp::I32Load16S { .. }
165                | WasmOp::I32Load16U { .. }
166                | WasmOp::I32Store { .. }
167                | WasmOp::I32Store8 { .. }
168                | WasmOp::I32Store16 { .. }
169                | WasmOp::I32TruncF32S
170                | WasmOp::I32TruncF32U
171        )
172    }
173
174    /// Verify equivalence between a WASM operation and ARM operations
175    pub fn verify_equivalence(
176        &self,
177        wasm_op: &WasmOp,
178        arm_ops: &[ArmOp],
179    ) -> Result<ValidationResult, VerificationError> {
180        self.verify_equivalence_parameterized(wasm_op, arm_ops, &[])
181    }
182
183    /// Verify equivalence with concrete parameter values
184    pub fn verify_equivalence_parameterized(
185        &self,
186        wasm_op: &WasmOp,
187        arm_ops: &[ArmOp],
188        concrete_params: &[(usize, i64)],
189    ) -> Result<ValidationResult, VerificationError> {
190        let mut solver = new_solver();
191
192        // Create inputs - some symbolic, some concrete
193        let num_inputs = self.get_num_inputs(wasm_op);
194        let mut inputs: Vec<BV> = Vec::new();
195
196        for i in 0..num_inputs {
197            let input = if let Some((_, value)) = concrete_params.iter().find(|(idx, _)| *idx == i)
198            {
199                // Concrete value
200                BV::from_i64(*value, 32)
201            } else {
202                // Symbolic value
203                BV::new_const(format!("input_{}", i), 32)
204            };
205            inputs.push(input);
206        }
207
208        // Encode WASM semantics
209        let wasm_result = self.wasm_encoder.encode_op(wasm_op, &inputs);
210
211        // Encode ARM semantics
212        let arm_result = self.encode_arm_sequence(arm_ops, &inputs)?;
213
214        // Assert that results are NOT equal
215        // If this is UNSAT, then the results are always equal (proven correct)
216        // If this is SAT, we found a counterexample
217        solver.assert(&wasm_result.eq(&arm_result).not());
218
219        match solver.check() {
220            CheckOutcome::Unsat => {
221                // Proven correct - no inputs exist where results differ
222                Ok(ValidationResult::Verified)
223            }
224
225            CheckOutcome::Sat => {
226                // Found counterexample: read the differing inputs back from
227                // the model (symbolic inputs only — concrete params have no
228                // model entry). Values are reported unsigned, as before.
229                let mut counterexample = Vec::new();
230                for (i, input) in inputs.iter().enumerate() {
231                    if let Some(value) = solver.value(input)
232                        && let Ok(int_val) = i64::try_from(value)
233                    {
234                        counterexample.push((format!("input_{}", i), int_val));
235                    }
236                }
237
238                Ok(ValidationResult::Invalid { counterexample })
239            }
240
241            CheckOutcome::Unknown(reason) => {
242                // Verification inconclusive
243                Ok(ValidationResult::Unknown {
244                    reason: format!("SMT solver returned unknown: {reason}"),
245                })
246            }
247        }
248    }
249
250    /// Encode a sequence of ARM operations
251    fn encode_arm_sequence(
252        &self,
253        arm_ops: &[ArmOp],
254        inputs: &[BV],
255    ) -> Result<BV, VerificationError> {
256        let mut state = ArmState::new_symbolic();
257
258        // Initialize input registers
259        for (i, input) in inputs.iter().enumerate() {
260            let reg = match i {
261                0 => Reg::R0,
262                1 => Reg::R1,
263                2 => Reg::R2,
264                _ => {
265                    return Err(VerificationError::UnsupportedOperation(format!(
266                        "Too many inputs: {}",
267                        inputs.len()
268                    )));
269                }
270            };
271            state.set_reg(&reg, input.clone());
272        }
273
274        // Execute ARM operations
275        for arm_op in arm_ops {
276            self.arm_encoder.encode_op(arm_op, &mut state);
277        }
278
279        // Extract result from R0 (ARM calling convention)
280        Ok(self.arm_encoder.extract_result(&state, &Reg::R0))
281    }
282
283    /// Verify operation for all parameter values in a range
284    pub fn verify_parameterized_range<F>(
285        &self,
286        wasm_op: &WasmOp,
287        create_arm_ops: F,
288        param_index: usize,
289        range: std::ops::Range<i64>,
290    ) -> Result<ValidationResult, VerificationError>
291    where
292        F: Fn(i64) -> Vec<ArmOp>,
293    {
294        for value in range {
295            let arm_ops = create_arm_ops(value);
296            let result =
297                self.verify_equivalence_parameterized(wasm_op, &arm_ops, &[(param_index, value)])?;
298
299            match result {
300                ValidationResult::Verified => continue,
301                ValidationResult::Invalid { counterexample } => {
302                    return Ok(ValidationResult::Invalid {
303                        counterexample: counterexample
304                            .into_iter()
305                            .map(|(k, v)| (format!("{} (param={})", k, value), v))
306                            .collect(),
307                    });
308                }
309                ValidationResult::Unknown { reason } => {
310                    return Ok(ValidationResult::Unknown {
311                        reason: format!("Failed at param={}: {}", value, reason),
312                    });
313                }
314            }
315        }
316
317        Ok(ValidationResult::Verified)
318    }
319
320    /// VCR-VER-002 (#166): mandatory **trap-preservation** obligation for the
321    /// partial-op lowerings whose ARM trap condition synth can DERIVE from the
322    /// emitted sequence. The ARM side is no longer a structural `Udf`-presence
323    /// proxy: [`ArmSemantics::encode_sequence_br`] threads a `may_trap` term
324    /// through the exec model — guard branches condition it, `UDF` execution
325    /// accumulates it — so a dropped, inverted, or wrong-register guard
326    /// derives a trap condition that fails the VC.
327    ///
328    /// # Covered classes (LIVE, derived ARM trap term)
329    ///
330    /// | class | VC | operand convention |
331    /// |---|---|---|
332    /// | i32 div/rem | full ([`crate::trap::prove_trap_equivalence`]: trap AND guarded value) | dividend R0, divisor R1, result R0 |
333    /// | `unreachable` | trap-condition only | none |
334    /// | i32 load/store (all widths) | trap-condition only (no memory-contents model) | address R0 (+ store value R1), linear-memory size R10 |
335    /// | `i32.trunc_f32_s/u` | trap-condition only (no float→int value model) | operand S0, result R0 |
336    ///
337    /// `call_indirect` goes through
338    /// [`Self::verify_call_indirect_trap_preservation`] (it needs module
339    /// facts as the spec side).
340    ///
341    /// # Held out, honestly
342    ///
343    /// - **i64 div/rem** — needs 64-bit operand terms + the register-pair ARM
344    ///   value model; still gated at the unit level
345    ///   (`tests/trap_preservation.rs`) and by the CI execution oracles.
346    /// - **`i32.trunc_f64_s/u`** — the guard drives f64 (D-register)
347    ///   compares, which the 32-bit VFP register model does not carry yet;
348    ///   unit-gated (`trap_trunc` classifier) + the m4f CI execution oracle.
349    pub fn verify_trap_preservation(
350        &self,
351        wasm_op: &WasmOp,
352        arm_ops: &[ArmOp],
353    ) -> Result<ValidationResult, VerificationError> {
354        match wasm_op {
355            WasmOp::I32DivS | WasmOp::I32DivU | WasmOp::I32RemS | WasmOp::I32RemU => {
356                self.verify_div_rem_trap_preservation(wasm_op, arm_ops)
357            }
358            WasmOp::Unreachable => {
359                let (_state, arm_trap) = self.derive_arm_state(arm_ops, &[], None)?;
360                Ok(Self::condition_verdict(
361                    &crate::trap::trap_always(),
362                    &arm_trap,
363                ))
364            }
365            WasmOp::I32Load { offset, .. }
366            | WasmOp::I32Load8S { offset, .. }
367            | WasmOp::I32Load8U { offset, .. }
368            | WasmOp::I32Load16S { offset, .. }
369            | WasmOp::I32Load16U { offset, .. }
370            | WasmOp::I32Store { offset, .. }
371            | WasmOp::I32Store8 { offset, .. }
372            | WasmOp::I32Store16 { offset, .. } => {
373                let size: u64 = match wasm_op {
374                    WasmOp::I32Load8S { .. }
375                    | WasmOp::I32Load8U { .. }
376                    | WasmOp::I32Store8 { .. } => 1,
377                    WasmOp::I32Load16S { .. }
378                    | WasmOp::I32Load16U { .. }
379                    | WasmOp::I32Store16 { .. } => 2,
380                    _ => 4,
381                };
382                self.verify_mem_trap_preservation(arm_ops, *offset, size)
383            }
384            WasmOp::I32TruncF32S | WasmOp::I32TruncF32U => {
385                let signed = matches!(wasm_op, WasmOp::I32TruncF32S);
386                self.verify_trunc_f32_trap_preservation(arm_ops, signed)
387            }
388            other => Err(VerificationError::UnsupportedOperation(format!(
389                "trap-preservation gate does not cover {other:?} \
390                 (i64 div/rem and trunc_f64 are unit-gated — see method docs)"
391            ))),
392        }
393    }
394
395    /// i32 div/rem trap preservation (VCR-VER-002, #166): full VC — the trap
396    /// clause AND the guarded value clause — with the ARM trap term DERIVED
397    /// from the emitted guard structure by the branch-taking executor.
398    /// Operands: dividend = `input_0` (R0), divisor = `input_1` (R1),
399    /// result in R0.
400    ///
401    /// The VALUE term comes from the straight-line pass
402    /// ([`ArmSemantics::encode_sequence_value_straightline`]) whenever the
403    /// sequence's branch structure is value-dead
404    /// ([`ArmSemantics::branch_spans_are_value_dead`] — true for every
405    /// shipped div/rem guard shape): on such sequences every non-trapping
406    /// path produces the same registers as straight-line execution, and the
407    /// resulting ite-free SDIV/UDIV/MLS terms stay STRUCTURALLY aligned with
408    /// the WASM encoding — an `ite(guard, …)` wrapper on a divider/multiplier
409    /// operand un-shares the circuits and sends the UNSAT value proof off a
410    /// CDCL cliff. Non-conforming shapes fall back to the guarded
411    /// (if-converted) post-state value — sound, potentially slow.
412    pub fn verify_div_rem_trap_preservation(
413        &self,
414        wasm_op: &WasmOp,
415        arm_ops: &[ArmOp],
416    ) -> Result<ValidationResult, VerificationError> {
417        let Some(div_op) = crate::trap::div_op(wasm_op) else {
418            return Err(VerificationError::UnsupportedOperation(format!(
419                "trap-preservation gate applies to div/rem only, got {wasm_op:?}"
420            )));
421        };
422        // This method models 32-bit operands only; `div_op` also maps the i64
423        // variants (VCR-VER-002 follow-on: i64 needs 64-bit operand terms +
424        // the register-pair ARM value model).
425        if !matches!(
426            wasm_op,
427            WasmOp::I32DivS | WasmOp::I32DivU | WasmOp::I32RemS | WasmOp::I32RemU
428        ) {
429            return Err(VerificationError::UnsupportedOperation(format!(
430                "trap-preservation gate currently supports i32 div/rem only, got {wasm_op:?}"
431            )));
432        }
433
434        // Symbolic operands, matching `verify_equivalence_parameterized`'s
435        // naming: dividend = input_0 (R0), divisor = input_1 (R1).
436        let dividend = BV::new_const("input_0", 32);
437        let divisor = BV::new_const("input_1", 32);
438        let inputs = vec![dividend.clone(), divisor.clone()];
439
440        let wasm_value = self.wasm_encoder.encode_op(wasm_op, &inputs);
441        let (state, arm_may_trap) = self.derive_arm_state(arm_ops, &inputs, None)?;
442        let arm_value = if ArmSemantics::branch_spans_are_value_dead(arm_ops) {
443            // Ite-free value term, structurally aligned with the WASM side
444            // (see the method doc for the soundness argument).
445            let mut vstate = ArmState::new_symbolic();
446            Self::seed_inputs(&mut vstate, &inputs)?;
447            self.arm_encoder
448                .encode_sequence_value_straightline(arm_ops, &mut vstate)
449                .map_err(VerificationError::UnsupportedOperation)?;
450            self.arm_encoder.extract_result(&vstate, &Reg::R0)
451        } else {
452            self.arm_encoder.extract_result(&state, &Reg::R0)
453        };
454
455        let orig = crate::trap::DefineOrTrap {
456            value: wasm_value,
457            may_trap: crate::trap::trap_div(div_op, &dividend, &divisor),
458        };
459        let opt = crate::trap::DefineOrTrap {
460            value: arm_value,
461            may_trap: arm_may_trap,
462        };
463
464        Ok(Self::trap_verdict_to_result(
465            crate::trap::prove_trap_equivalence(&orig, &opt),
466        ))
467    }
468
469    /// i32 load/store OOB trap preservation (VCR-VER-002, #166 / #377):
470    /// trap-condition-only VC (synth models no memory contents). The WASM
471    /// side is ordeal's wraparound-safe bound check on the effective address
472    /// (`addr + offset + size >u mem_size`, exact 33-bit arithmetic); the ARM
473    /// side is DERIVED from the emitted software-bounds guard. Operand
474    /// convention: address = `input_0` (R0), store value (if any) = `input_1`
475    /// (R1), linear-memory size = R10 (the shipped ABI register).
476    pub fn verify_mem_trap_preservation(
477        &self,
478        arm_ops: &[ArmOp],
479        offset: u32,
480        access_size: u64,
481    ) -> Result<ValidationResult, VerificationError> {
482        let addr = BV::new_const("input_0", 32);
483        let value = BV::new_const("input_1", 32);
484        let inputs = vec![addr.clone(), value];
485
486        let mut state = ArmState::new_symbolic();
487        // The WASM-side bound is THE SAME symbol the ARM guard compares
488        // against: R10 = linear-memory size in bytes (shipped ABI).
489        let mem_bound = state.get_reg(&Reg::R10).clone();
490        Self::seed_inputs(&mut state, &inputs)?;
491        self.arm_encoder
492            .encode_sequence_br(arm_ops, &mut state)
493            .map_err(VerificationError::UnsupportedOperation)?;
494        let arm_trap = state.may_trap.clone();
495
496        // WASM Core: ea = addr + offset (exact), trap iff ea + size > bound.
497        // Folding the static offset into the size operand keeps ordeal's
498        // 33-bit zero-extended arithmetic exact: zext(addr) + zext(off+size).
499        let static_bytes = offset as u64 + access_size;
500        let wasm_trap = if static_bytes > u32::MAX as u64 {
501            // offset + size alone exceeds the 32-bit bound: every access traps.
502            crate::trap::trap_always()
503        } else {
504            crate::trap::trap_mem_oob(&addr, &BV::from_u64(static_bytes, 32), &mem_bound)
505        };
506
507        Ok(Self::condition_verdict(&wasm_trap, &arm_trap))
508    }
509
510    /// `i32.trunc_f32_s/u` trap preservation (VCR-VER-002, #166 / #709):
511    /// trap-condition-only VC (synth's QF_BV model carries no float→int
512    /// value function). The WASM side is ordeal 0.9.1's bit-pattern trunc
513    /// classifier (`NaN ∨ ±∞ ∨ out-of-range`); the ARM side is DERIVED from
514    /// the emitted domain guard (`F32Const` bound + ordered VFP compare +
515    /// `Cmp`/branch/`Udf`), with the ordered compares given real bit-pattern
516    /// semantics in the executor. Operand convention: float operand = S0.
517    pub fn verify_trunc_f32_trap_preservation(
518        &self,
519        arm_ops: &[ArmOp],
520        signed: bool,
521    ) -> Result<ValidationResult, VerificationError> {
522        use synth_synthesis::rules::VfpReg;
523        let bits = BV::new_const("input_0", 32);
524
525        let mut state = ArmState::new_symbolic();
526        state.set_vfp_reg(&VfpReg::S0, bits.clone());
527        self.arm_encoder
528            .encode_sequence_br(arm_ops, &mut state)
529            .map_err(VerificationError::UnsupportedOperation)?;
530        let arm_trap = state.may_trap.clone();
531
532        let wasm_trap = crate::trap::trap_trunc(
533            &bits,
534            crate::trap::FpFmt::F32,
535            crate::trap::IntTarget::I32,
536            signed,
537        );
538
539        Ok(Self::condition_verdict(&wasm_trap, &arm_trap))
540    }
541
542    /// `call_indirect` trap preservation (VCR-VER-002, #166 / #642 #664 #676):
543    /// trap-condition-only VC. The WASM-side spec comes from MODULE FACTS the
544    /// caller supplies ([`CallIndirectSpec`]); the ARM side is derived from
545    /// the `ArmOp::CallIndirect` pseudo-op's guard fields (`table_size`,
546    /// `null_check`, `type_check`) through the SAME pinned ordeal builder —
547    /// a selector that resolves the wrong table size, drops the null check on
548    /// a table with uninitialized slots (#664), or skips the runtime type
549    /// check on a heterogeneous table (#676) is reported `Invalid`.
550    ///
551    /// # Trust boundary
552    ///
553    /// This certifies the SELECTOR's guard resolution at the pseudo-op level;
554    /// the encoder's expansion of those fields into `CMP`/`BLO`/`UDF`/`BLX`
555    /// bytes is separately execution-gated (the unicorn call_indirect CI
556    /// jobs). Statically-discharged clauses are modeled by a provably
557    /// non-null slot term (`slot | 1`), keeping ordeal's builder the single
558    /// spec source.
559    pub fn verify_call_indirect_trap_preservation(
560        &self,
561        arm_op: &ArmOp,
562        spec: &CallIndirectSpec,
563    ) -> Result<ValidationResult, VerificationError> {
564        let ArmOp::CallIndirect {
565            table_size,
566            null_check,
567            type_check,
568            ..
569        } = arm_op
570        else {
571            return Err(VerificationError::UnsupportedOperation(format!(
572                "call_indirect trap gate needs the CallIndirect pseudo-op, got {arm_op:?}"
573            )));
574        };
575
576        let index = BV::new_const("input_0", 32);
577        let slot = BV::new_const("slot_ptr", 32);
578        let nonnull_slot = slot.bvor(BV::from_u64(1, 32));
579        let actual_ty = BV::new_const("slot_type_id", 32);
580
581        let build = |size: u32, may_null: bool, expected: Option<u32>| {
582            let expected_bv = expected.map(|e| BV::from_u64(e as u64, 32));
583            let size_bv = BV::from_u64(size as u64, 32);
584            let slot_term = if may_null { &slot } else { &nonnull_slot };
585            let type_trap = match &expected_bv {
586                Some(e) => crate::trap::TypeTrap::Runtime {
587                    actual_type_id: &actual_ty,
588                    expected_id: e,
589                },
590                None => crate::trap::TypeTrap::StaticallyDischarged,
591            };
592            crate::trap::trap_call_indirect(&crate::trap::CallIndirect {
593                index: &index,
594                table_size: &size_bv,
595                slot_ptr: slot_term,
596                type_trap,
597            })
598        };
599
600        let wasm_trap = build(
601            spec.table_size,
602            spec.may_have_null_slot,
603            spec.heterogeneous_expected_type,
604        );
605        let arm_trap = build(
606            *table_size,
607            *null_check,
608            type_check.as_ref().map(|(expected, _)| *expected),
609        );
610
611        Ok(Self::condition_verdict(&wasm_trap, &arm_trap))
612    }
613
614    /// Seed integer operand registers (R0..R2) and run the branch-taking
615    /// executor, returning the post-state and the DERIVED trap condition.
616    fn derive_arm_state(
617        &self,
618        arm_ops: &[ArmOp],
619        inputs: &[BV],
620        vfp_s0: Option<&BV>,
621    ) -> Result<(ArmState, Bool), VerificationError> {
622        let mut state = ArmState::new_symbolic();
623        Self::seed_inputs(&mut state, inputs)?;
624        if let Some(bits) = vfp_s0 {
625            state.set_vfp_reg(&synth_synthesis::rules::VfpReg::S0, bits.clone());
626        }
627        self.arm_encoder
628            .encode_sequence_br(arm_ops, &mut state)
629            .map_err(VerificationError::UnsupportedOperation)?;
630        let trap = state.may_trap.clone();
631        Ok((state, trap))
632    }
633
634    fn seed_inputs(state: &mut ArmState, inputs: &[BV]) -> Result<(), VerificationError> {
635        for (i, input) in inputs.iter().enumerate() {
636            let reg = match i {
637                0 => Reg::R0,
638                1 => Reg::R1,
639                2 => Reg::R2,
640                _ => {
641                    return Err(VerificationError::UnsupportedOperation(format!(
642                        "Too many inputs: {}",
643                        inputs.len()
644                    )));
645                }
646            };
647            state.set_reg(&reg, input.clone());
648        }
649        Ok(())
650    }
651
652    /// Run the trap-condition-only VC and map the verdict.
653    fn condition_verdict(wasm_trap: &Bool, arm_trap: &Bool) -> ValidationResult {
654        Self::trap_verdict_to_result(crate::trap::prove_trap_condition_equivalence(
655            wasm_trap, arm_trap,
656        ))
657    }
658
659    fn trap_verdict_to_result(verdict: crate::trap::TrapVerdict) -> ValidationResult {
660        match verdict {
661            crate::trap::TrapVerdict::Preserved => ValidationResult::Verified,
662            crate::trap::TrapVerdict::Dropped(model) => ValidationResult::Invalid {
663                counterexample: model
664                    .into_iter()
665                    .filter_map(|(n, v)| i64::try_from(v).ok().map(|x| (n, x)))
666                    .collect(),
667            },
668            crate::trap::TrapVerdict::Unknown => ValidationResult::Unknown {
669                reason: "trap-preservation VC returned Unknown (conservative reject)".to_string(),
670            },
671        }
672    }
673
674    /// Get number of inputs required for a WASM operation
675    fn get_num_inputs(&self, wasm_op: &WasmOp) -> usize {
676        use WasmOp::*;
677        match wasm_op {
678            // Binary operations
679            I32Add | I32Sub | I32Mul | I32DivS | I32DivU | I32RemS | I32RemU | I32And | I32Or
680            | I32Xor | I32Shl | I32ShrS | I32ShrU | I32Rotl | I32Rotr | I32Eq | I32Ne | I32LtS
681            | I32LtU | I32LeS | I32LeU | I32GtS | I32GtU | I32GeS | I32GeU => 2,
682
683            // Unary operations
684            I32Clz | I32Ctz | I32Popcnt | I32Eqz => 1,
685
686            // Constants
687            I32Const(_) => 0,
688
689            // Memory operations
690            I32Load { .. } => 1,  // address
691            I32Store { .. } => 2, // address + value
692
693            // Control flow
694            LocalGet(_) | GlobalGet(_) => 0,
695            LocalSet(_) | GlobalSet(_) | LocalTee(_) => 1,
696            Br(_) | BrIf(_) | Return => 0,
697
698            // Other operations
699            Drop => 1,
700            Select => 3, // condition + two values
701            Nop | Unreachable | Block | Loop | If | Else | End => 0,
702
703            // Default for unknown
704            _ => 0,
705        }
706    }
707
708    /// Batch verify multiple synthesis rules
709    pub fn verify_rules(
710        &self,
711        rules: &[SynthesisRule],
712    ) -> Vec<(String, Result<ValidationResult, VerificationError>)> {
713        rules
714            .iter()
715            .map(|rule| {
716                let result = self.verify_rule(rule);
717                (rule.name.clone(), result)
718            })
719            .collect()
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726    use crate::with_verification_context;
727    use synth_synthesis::rules::Condition;
728    use synth_synthesis::{Cost, Operand2, Pattern, Replacement};
729
730    fn create_test_rule(wasm_op: WasmOp, arm_op: ArmOp) -> SynthesisRule {
731        SynthesisRule {
732            name: format!("{:?}", wasm_op),
733            priority: 0,
734            pattern: Pattern::WasmInstr(wasm_op),
735            replacement: Replacement::ArmInstr(arm_op),
736            cost: Cost {
737                cycles: 1,
738                code_size: 4,
739                registers: 2,
740            },
741        }
742    }
743
744    // --- VCR-VER-002 (#166): div/rem trap-preservation wired into the validator ---
745
746    #[test]
747    fn div_lowering_without_guard_is_rejected_as_trap_drop() {
748        with_verification_context(|| {
749            let validator = TranslationValidator::new();
750            // Bare UDIV — the value is right but the ÷0 guard is missing
751            // (the #633/#666 shape). The trap-preservation gate must reject it.
752            let arm_ops = [ArmOp::Udiv {
753                rd: Reg::R0,
754                rn: Reg::R0,
755                rm: Reg::R1,
756            }];
757            let result = validator
758                .verify_div_rem_trap_preservation(&WasmOp::I32DivU, &arm_ops)
759                .unwrap();
760            match result {
761                ValidationResult::Invalid { counterexample } => {
762                    // The counterexample must exhibit the dropped trap: divisor 0.
763                    let divisor = counterexample.iter().find(|(n, _)| n == "input_1");
764                    assert_eq!(
765                        divisor.map(|(_, v)| *v),
766                        Some(0),
767                        "trap-drop counterexample must set the divisor to 0"
768                    );
769                }
770                other => panic!("unguarded div must be Invalid, got {other:?}"),
771            }
772        });
773    }
774
775    /// The SHIPPED ÷0 guard shape (instruction_selector.rs I32DivU /
776    /// optimizer_bridge.rs DivU): CMP divisor,#0 ; BNE +0 (skip the UDF) ;
777    /// UDF ; UDIV. The derived trap term is (divisor == 0) — exactly WASM's.
778    fn shipped_divu_guard() -> Vec<ArmOp> {
779        vec![
780            ArmOp::Cmp {
781                rn: Reg::R1,
782                op2: Operand2::Imm(0),
783            },
784            ArmOp::BCondOffset {
785                cond: Condition::NE,
786                offset: 0,
787            },
788            ArmOp::Udf { imm: 0 },
789            ArmOp::Udiv {
790                rd: Reg::R0,
791                rn: Reg::R0,
792                rm: Reg::R1,
793            },
794        ]
795    }
796
797    /// The SHIPPED div_s DOUBLE guard (optimizer_bridge.rs DivS): the ÷0
798    /// guard plus the INT_MIN/-1 overflow guard (MOVW/MOVT 0x80000000 into
799    /// R12 ; CMP dividend ; BNE +3 ; CMN divisor,#1 ; BNE +0 ; UDF #1).
800    fn shipped_divs_double_guard() -> Vec<ArmOp> {
801        vec![
802            ArmOp::Cmp {
803                rn: Reg::R1,
804                op2: Operand2::Imm(0),
805            },
806            ArmOp::BCondOffset {
807                cond: Condition::NE,
808                offset: 0,
809            },
810            ArmOp::Udf { imm: 0 },
811            ArmOp::Movw {
812                rd: Reg::R12,
813                imm16: 0,
814            },
815            ArmOp::Movt {
816                rd: Reg::R12,
817                imm16: 0x8000,
818            },
819            ArmOp::Cmp {
820                rn: Reg::R0,
821                op2: Operand2::Reg(Reg::R12),
822            },
823            ArmOp::BCondOffset {
824                cond: Condition::NE,
825                offset: 3,
826            },
827            ArmOp::Cmn {
828                rn: Reg::R1,
829                op2: Operand2::Imm(1),
830            },
831            ArmOp::BCondOffset {
832                cond: Condition::NE,
833                offset: 0,
834            },
835            ArmOp::Udf { imm: 1 },
836            ArmOp::Sdiv {
837                rd: Reg::R0,
838                rn: Reg::R0,
839                rm: Reg::R1,
840            },
841        ]
842    }
843
844    #[test]
845    fn div_lowering_with_guard_preserves_the_trap() {
846        with_verification_context(|| {
847            let validator = TranslationValidator::new();
848            let result = validator
849                .verify_div_rem_trap_preservation(&WasmOp::I32DivU, &shipped_divu_guard())
850                .unwrap();
851            assert_eq!(result, ValidationResult::Verified);
852        });
853    }
854
855    /// The derived gate is STRICTER than the retired Udf-presence proxy: a
856    /// guard with the branch polarity inverted (BEQ instead of BNE — the UDF
857    /// fires exactly when the divide is fine) still contains a Udf, so the
858    /// proxy called it Verified; the derived trap term is (divisor != 0),
859    /// which fails the VC.
860    #[test]
861    fn div_guard_with_inverted_polarity_is_rejected() {
862        with_verification_context(|| {
863            let validator = TranslationValidator::new();
864            let mut arm_ops = shipped_divu_guard();
865            arm_ops[1] = ArmOp::BCondOffset {
866                cond: Condition::EQ,
867                offset: 0,
868            };
869            let result = validator
870                .verify_div_rem_trap_preservation(&WasmOp::I32DivU, &arm_ops)
871                .unwrap();
872            assert!(
873                matches!(result, ValidationResult::Invalid { .. }),
874                "inverted guard polarity must be Invalid, got {result:?}"
875            );
876        });
877    }
878
879    #[test]
880    fn signed_div_double_guard_preserves_both_zero_and_overflow_traps() {
881        with_verification_context(|| {
882            let validator = TranslationValidator::new();
883            let result = validator
884                .verify_div_rem_trap_preservation(&WasmOp::I32DivS, &shipped_divs_double_guard())
885                .unwrap();
886            assert_eq!(result, ValidationResult::Verified);
887        });
888    }
889
890    /// RED-FIRST for the div_s overflow class (#633-shape at i32): stripping
891    /// ONLY the INT_MIN/-1 overflow guard keeps a Udf in the sequence (the ÷0
892    /// guard), so the retired structural proxy called this Verified. The
893    /// derived trap term is only (divisor == 0), and the VC finds the dropped
894    /// overflow trap with the INT_MIN/-1 counterexample.
895    #[test]
896    fn signed_div_with_overflow_guard_stripped_is_rejected() {
897        with_verification_context(|| {
898            let validator = TranslationValidator::new();
899            let arm_ops = [
900                ArmOp::Cmp {
901                    rn: Reg::R1,
902                    op2: Operand2::Imm(0),
903                },
904                ArmOp::BCondOffset {
905                    cond: Condition::NE,
906                    offset: 0,
907                },
908                ArmOp::Udf { imm: 0 },
909                ArmOp::Sdiv {
910                    rd: Reg::R0,
911                    rn: Reg::R0,
912                    rm: Reg::R1,
913                },
914            ];
915            let result = validator
916                .verify_div_rem_trap_preservation(&WasmOp::I32DivS, &arm_ops)
917                .unwrap();
918            match result {
919                ValidationResult::Invalid { counterexample } => {
920                    let get = |n: &str| {
921                        counterexample
922                            .iter()
923                            .find(|(name, _)| name == n)
924                            .map(|(_, v)| *v)
925                    };
926                    assert_eq!(
927                        get("input_0"),
928                        Some(i32::MIN as u32 as i64),
929                        "dropped overflow trap must exhibit dividend INT_MIN: {counterexample:?}"
930                    );
931                    assert_eq!(
932                        get("input_1"),
933                        Some(u32::MAX as i64),
934                        "dropped overflow trap must exhibit divisor -1: {counterexample:?}"
935                    );
936                }
937                other => panic!("overflow-guard-stripped div_s must be Invalid, got {other:?}"),
938            }
939        });
940    }
941
942    /// rem_s carries ONLY the ÷0 guard — WASM rem_s(INT_MIN, -1) is 0, not a
943    /// trap — and the derived gate agrees.
944    #[test]
945    fn rems_single_zero_guard_is_exactly_right() {
946        with_verification_context(|| {
947            let validator = TranslationValidator::new();
948            let arm_ops = [
949                ArmOp::Cmp {
950                    rn: Reg::R1,
951                    op2: Operand2::Imm(0),
952                },
953                ArmOp::BCondOffset {
954                    cond: Condition::NE,
955                    offset: 0,
956                },
957                ArmOp::Udf { imm: 0 },
958                ArmOp::Sdiv {
959                    rd: Reg::R2,
960                    rn: Reg::R0,
961                    rm: Reg::R1,
962                },
963                ArmOp::Mls {
964                    rd: Reg::R0,
965                    rn: Reg::R2,
966                    rm: Reg::R1,
967                    ra: Reg::R0,
968                },
969            ];
970            let result = validator
971                .verify_div_rem_trap_preservation(&WasmOp::I32RemS, &arm_ops)
972                .unwrap();
973            assert_eq!(result, ValidationResult::Verified);
974        });
975    }
976
977    // --- unreachable (LIVE, #665 class) ---
978
979    #[test]
980    fn unreachable_udf_lowering_preserves_the_trap() {
981        with_verification_context(|| {
982            let validator = TranslationValidator::new();
983            let result = validator
984                .verify_trap_preservation(&WasmOp::Unreachable, &[ArmOp::Udf { imm: 0 }])
985                .unwrap();
986            assert_eq!(result, ValidationResult::Verified);
987        });
988    }
989
990    #[test]
991    fn unreachable_lowered_to_nop_is_rejected() {
992        with_verification_context(|| {
993            let validator = TranslationValidator::new();
994            // The #665 shape: unreachable silently became a no-op.
995            let result = validator
996                .verify_trap_preservation(&WasmOp::Unreachable, &[ArmOp::Nop])
997                .unwrap();
998            assert!(
999                matches!(result, ValidationResult::Invalid { .. }),
1000                "trap-dropping unreachable lowering must be Invalid, got {result:?}"
1001            );
1002        });
1003    }
1004
1005    // --- i32 load/store OOB (LIVE, #377/#752 class) ---
1006
1007    /// The SHIPPED software-bounds lowering at the gate's register
1008    /// convention (address = R0 = `input_0`): the guard prefix comes from
1009    /// `InstructionSelector::software_bounds_guard` — THE function every
1010    /// emission site calls, public precisely so this gate pins the real
1011    /// shipped shape and no hand-maintained mirror can drift (the VCR-ORACLE
1012    /// lesson) — composed with the trailing access op per the generators'
1013    /// documented contract (`result.last() is Ldr/Ldrb/.../Str
1014    /// [R11, addr, #offset]`, pinned by the selector unit tests). The full
1015    /// selector is not used directly because it homes a bare op's address in
1016    /// an allocator-chosen register, not the gate's `input_0`.
1017    fn shipped_software_bounds_ops(wasm_op: &WasmOp) -> Vec<ArmOp> {
1018        use synth_synthesis::instruction_selector::InstructionSelector;
1019        use synth_synthesis::rules::MemAddr;
1020        let (offset, size) = match wasm_op {
1021            WasmOp::I32Load { offset, .. } | WasmOp::I32Store { offset, .. } => (*offset, 4u32),
1022            WasmOp::I32Load16S { offset, .. }
1023            | WasmOp::I32Load16U { offset, .. }
1024            | WasmOp::I32Store16 { offset, .. } => (*offset, 2),
1025            WasmOp::I32Load8S { offset, .. }
1026            | WasmOp::I32Load8U { offset, .. }
1027            | WasmOp::I32Store8 { offset, .. } => (*offset, 1),
1028            other => panic!("not a guarded i32 access: {other:?}"),
1029        };
1030        let addr = MemAddr::reg_imm(Reg::R11, Reg::R0, offset as i32);
1031        let access = match wasm_op {
1032            WasmOp::I32Load { .. } => ArmOp::Ldr { rd: Reg::R0, addr },
1033            WasmOp::I32Load8S { .. } => ArmOp::Ldrsb { rd: Reg::R0, addr },
1034            WasmOp::I32Load8U { .. } => ArmOp::Ldrb { rd: Reg::R0, addr },
1035            WasmOp::I32Load16S { .. } => ArmOp::Ldrsh { rd: Reg::R0, addr },
1036            WasmOp::I32Load16U { .. } => ArmOp::Ldrh { rd: Reg::R0, addr },
1037            WasmOp::I32Store { .. } => ArmOp::Str { rd: Reg::R1, addr },
1038            WasmOp::I32Store8 { .. } => ArmOp::Strb { rd: Reg::R1, addr },
1039            WasmOp::I32Store16 { .. } => ArmOp::Strh { rd: Reg::R1, addr },
1040            other => panic!("not a guarded i32 access: {other:?}"),
1041        };
1042        let mut ops = InstructionSelector::software_bounds_guard(Reg::R0, offset as i32, size);
1043        ops.push(access);
1044        ops
1045    }
1046
1047    /// RED-FIRST: a load with the bounds guard stripped derives trap = false
1048    /// and is rejected against the WASM OOB condition.
1049    #[test]
1050    fn load_without_bounds_guard_is_rejected() {
1051        use synth_synthesis::rules::MemAddr;
1052        with_verification_context(|| {
1053            let validator = TranslationValidator::new();
1054            let arm_ops = [ArmOp::Ldr {
1055                rd: Reg::R0,
1056                addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
1057            }];
1058            let result = validator
1059                .verify_trap_preservation(
1060                    &WasmOp::I32Load {
1061                        offset: 0,
1062                        align: 2,
1063                    },
1064                    &arm_ops,
1065                )
1066                .unwrap();
1067            assert!(
1068                matches!(result, ValidationResult::Invalid { .. }),
1069                "guard-stripped load must be Invalid, got {result:?}"
1070            );
1071        });
1072    }
1073
1074    /// A byte load's shipped guard is exact for size 1 / offset 0 (it was the
1075    /// ONE class the old ADD-computed guard already got right) and stays
1076    /// Verified under the #752 wraparound-safe shape.
1077    #[test]
1078    fn byte_load_software_bounds_guard_preserves_the_trap() {
1079        with_verification_context(|| {
1080            let validator = TranslationValidator::new();
1081            let result = validator
1082                .verify_trap_preservation(
1083                    &WasmOp::I32Load8U {
1084                        offset: 0,
1085                        align: 0,
1086                    },
1087                    &shipped_software_bounds_ops(&WasmOp::I32Load8U {
1088                        offset: 0,
1089                        align: 0,
1090                    }),
1091                )
1092                .unwrap();
1093            assert_eq!(result, ValidationResult::Verified);
1094        });
1095    }
1096
1097    /// #752 CLOSED (was the pinned `word_load_software_bounds_guard_wraps_at_
1098    /// address_top` finding): the old shape computed `addr + (offset+size-1)`
1099    /// in WRAPPING 32-bit arithmetic, so at `addr >= 0x1_0000_0000 -
1100    /// (offset+size-1)` the end address wrapped small, the BLO guard passed,
1101    /// and the access escaped below the linear-memory base — the derived gate
1102    /// exhibited the dropped-trap counterexample at `addr >= 0xFFFF_FFFD`.
1103    /// The shipped guard is now the wraparound-safe SUB-from-bound shape
1104    /// (`software_bounds_guard`): this asserts the WHOLE class Verified, i.e.
1105    /// the divergence is unsatisfiable for EVERY addr including the top of
1106    /// the address space.
1107    #[test]
1108    fn word_load_software_bounds_guard_survives_the_address_top_752() {
1109        with_verification_context(|| {
1110            let validator = TranslationValidator::new();
1111            let result = validator
1112                .verify_trap_preservation(
1113                    &WasmOp::I32Load {
1114                        offset: 0,
1115                        align: 2,
1116                    },
1117                    &shipped_software_bounds_ops(&WasmOp::I32Load {
1118                        offset: 0,
1119                        align: 2,
1120                    }),
1121                )
1122                .unwrap();
1123            assert_eq!(
1124                result,
1125                ValidationResult::Verified,
1126                "the #752 wraparound divergence must be closed for every addr"
1127            );
1128        });
1129    }
1130
1131    /// #752: every guarded access width and a non-zero static offset verify —
1132    /// the wrap escape was specifically the multi-byte / non-zero-offset
1133    /// class, so gate the whole family (loads via the shipped selector).
1134    #[test]
1135    fn all_load_widths_software_bounds_guard_verify_752() {
1136        let cases: Vec<WasmOp> = vec![
1137            WasmOp::I32Load {
1138                offset: 4,
1139                align: 2,
1140            },
1141            WasmOp::I32Load8S {
1142                offset: 3,
1143                align: 0,
1144            },
1145            WasmOp::I32Load8U {
1146                offset: 1,
1147                align: 0,
1148            },
1149            WasmOp::I32Load16S {
1150                offset: 2,
1151                align: 1,
1152            },
1153            WasmOp::I32Load16U {
1154                offset: 0,
1155                align: 1,
1156            },
1157        ];
1158        with_verification_context(|| {
1159            let validator = TranslationValidator::new();
1160            for wasm_op in &cases {
1161                let result = validator
1162                    .verify_trap_preservation(wasm_op, &shipped_software_bounds_ops(wasm_op))
1163                    .unwrap();
1164                assert_eq!(result, ValidationResult::Verified, "{wasm_op:?}");
1165            }
1166        });
1167    }
1168
1169    /// #752: the store guards use the same `software_bounds_guard` prefix —
1170    /// all three widths, with non-zero static offsets on the subword forms.
1171    #[test]
1172    fn store_software_bounds_guard_verifies_752() {
1173        let cases: Vec<WasmOp> = vec![
1174            WasmOp::I32Store {
1175                offset: 0,
1176                align: 2,
1177            },
1178            WasmOp::I32Store8 {
1179                offset: 5,
1180                align: 0,
1181            },
1182            WasmOp::I32Store16 {
1183                offset: 3,
1184                align: 1,
1185            },
1186        ];
1187        with_verification_context(|| {
1188            let validator = TranslationValidator::new();
1189            for wasm_op in &cases {
1190                let result = validator
1191                    .verify_trap_preservation(wasm_op, &shipped_software_bounds_ops(wasm_op))
1192                    .unwrap();
1193                assert_eq!(result, ValidationResult::Verified, "{wasm_op:?}");
1194            }
1195        });
1196    }
1197
1198    /// #752: a static offset past the SUBW imm12 reach takes the MOVW
1199    /// materialization arm of the guard — Verified, i.e. the register-built
1200    /// constant is derived with the same exactness as the immediate form.
1201    #[test]
1202    fn large_offset_software_bounds_guard_verifies_752() {
1203        let wasm_op = WasmOp::I32Load {
1204            offset: 0x2000,
1205            align: 2,
1206        };
1207        with_verification_context(|| {
1208            let validator = TranslationValidator::new();
1209            let result = validator
1210                .verify_trap_preservation(&wasm_op, &shipped_software_bounds_ops(&wasm_op))
1211                .unwrap();
1212            assert_eq!(result, ValidationResult::Verified);
1213        });
1214    }
1215
1216    /// #752: `offset + size > u32::MAX` can never be in bounds — the guard
1217    /// degenerates to an unconditional UDF and the gate agrees with WASM's
1218    /// always-trap on the class.
1219    #[test]
1220    fn offset_overflow_software_bounds_guard_always_traps_752() {
1221        let wasm_op = WasmOp::I32Load {
1222            offset: u32::MAX,
1223            align: 2,
1224        };
1225        with_verification_context(|| {
1226            let validator = TranslationValidator::new();
1227            let result = validator
1228                .verify_trap_preservation(&wasm_op, &shipped_software_bounds_ops(&wasm_op))
1229                .unwrap();
1230            assert_eq!(result, ValidationResult::Verified);
1231        });
1232    }
1233
1234    /// REGRESSION PIN (the #752 finding itself): the RETIRED ADD-computed
1235    /// guard shape must stay Invalid, with the counterexample at the top of
1236    /// the address space — proving the gate still catches the wraparound
1237    /// class if anything ever re-emits it.
1238    #[test]
1239    fn retired_add_computed_guard_stays_invalid_at_the_address_top_752() {
1240        use synth_synthesis::rules::{Condition, MemAddr, Operand2};
1241        with_verification_context(|| {
1242            let validator = TranslationValidator::new();
1243            let arm_ops = [
1244                ArmOp::Add {
1245                    rd: Reg::R12,
1246                    rn: Reg::R0,
1247                    op2: Operand2::Imm(3), // offset 0 + size 4 - 1
1248                },
1249                ArmOp::Cmp {
1250                    rn: Reg::R12,
1251                    op2: Operand2::Reg(Reg::R10),
1252                },
1253                ArmOp::BCondOffset {
1254                    cond: Condition::LO,
1255                    offset: 0,
1256                },
1257                ArmOp::Udf { imm: 0 },
1258                ArmOp::Ldr {
1259                    rd: Reg::R0,
1260                    addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
1261                },
1262            ];
1263            let result = validator
1264                .verify_trap_preservation(
1265                    &WasmOp::I32Load {
1266                        offset: 0,
1267                        align: 2,
1268                    },
1269                    &arm_ops,
1270                )
1271                .unwrap();
1272            match result {
1273                ValidationResult::Invalid { counterexample } => {
1274                    let addr = counterexample
1275                        .iter()
1276                        .find(|(n, _)| n == "input_0")
1277                        .map(|(_, v)| *v)
1278                        .expect("counterexample must assign the address");
1279                    assert!(
1280                        addr >= 0xFFFF_FFFD,
1281                        "the divergence is the 32-bit end-address wrap at the top \
1282                         of the address space, got addr {addr:#x}"
1283                    );
1284                }
1285                other => panic!("the retired wrapping guard must stay Invalid, got {other:?}"),
1286            }
1287        });
1288    }
1289
1290    /// The gate is satisfiable by more than one correct guard: the issue's
1291    /// reference shape (trap iff bound < k, else iff addr >u bound - k,
1292    /// k = offset+size) also verifies — the mem-OOB class gate is not pinned
1293    /// to the shipped shape.
1294    #[test]
1295    fn wraparound_safe_bounds_guard_verifies() {
1296        use synth_synthesis::rules::MemAddr;
1297        with_verification_context(|| {
1298            let validator = TranslationValidator::new();
1299            let k = 4; // offset 0, size 4
1300            let arm_ops = [
1301                // CMP R10, #k ; BHS +0 ; UDF   — bound < k ⇒ every access traps
1302                ArmOp::Cmp {
1303                    rn: Reg::R10,
1304                    op2: Operand2::Imm(k),
1305                },
1306                ArmOp::BCondOffset {
1307                    cond: Condition::HS,
1308                    offset: 0,
1309                },
1310                ArmOp::Udf { imm: 0 },
1311                // SUB R12, R10, #k ; CMP addr, R12 ; BLS +0 ; UDF — exact on
1312                // the bound >= k path (no wrap possible)
1313                ArmOp::Sub {
1314                    rd: Reg::R12,
1315                    rn: Reg::R10,
1316                    op2: Operand2::Imm(k),
1317                },
1318                ArmOp::Cmp {
1319                    rn: Reg::R0,
1320                    op2: Operand2::Reg(Reg::R12),
1321                },
1322                ArmOp::BCondOffset {
1323                    cond: Condition::LS,
1324                    offset: 0,
1325                },
1326                ArmOp::Udf { imm: 0 },
1327                ArmOp::Ldr {
1328                    rd: Reg::R0,
1329                    addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
1330                },
1331            ];
1332            let result = validator
1333                .verify_trap_preservation(
1334                    &WasmOp::I32Load {
1335                        offset: 0,
1336                        align: 2,
1337                    },
1338                    &arm_ops,
1339                )
1340                .unwrap();
1341            assert_eq!(result, ValidationResult::Verified);
1342        });
1343    }
1344
1345    // --- i32.trunc_f32_s/u (LIVE, #709 class) ---
1346
1347    /// The SHIPPED trunc domain-guard shape (instruction_selector.rs
1348    /// I32TruncF32S/U): per bound, F32Const scratch ; ordered compare into
1349    /// R0 ; CMP R0,#0 ; BNE +0 (in-range skips) ; UDF — then the VCVT.
1350    /// Operand in S0, bound scratch S1 (the gate's register convention).
1351    fn shipped_trunc_f32_guard(signed: bool) -> Vec<ArmOp> {
1352        use synth_synthesis::rules::VfpReg;
1353        let (hi, lo) = if signed {
1354            (2147483648.0_f32, -2147483648.0_f32)
1355        } else {
1356            (4294967296.0_f32, -1.0_f32)
1357        };
1358        let mut ops = Vec::new();
1359        let guard = |ops: &mut Vec<ArmOp>, bound: f32, upper: bool| {
1360            ops.push(ArmOp::F32Const {
1361                sd: VfpReg::S1,
1362                value: bound,
1363            });
1364            let cmp = if upper {
1365                ArmOp::F32Lt {
1366                    rd: Reg::R0,
1367                    sn: VfpReg::S0,
1368                    sm: VfpReg::S1,
1369                }
1370            } else if signed {
1371                ArmOp::F32Ge {
1372                    rd: Reg::R0,
1373                    sn: VfpReg::S0,
1374                    sm: VfpReg::S1,
1375                }
1376            } else {
1377                ArmOp::F32Gt {
1378                    rd: Reg::R0,
1379                    sn: VfpReg::S0,
1380                    sm: VfpReg::S1,
1381                }
1382            };
1383            ops.push(cmp);
1384            ops.push(ArmOp::Cmp {
1385                rn: Reg::R0,
1386                op2: Operand2::Imm(0),
1387            });
1388            ops.push(ArmOp::BCondOffset {
1389                cond: Condition::NE,
1390                offset: 0,
1391            });
1392            ops.push(ArmOp::Udf { imm: 0 });
1393        };
1394        guard(&mut ops, hi, true);
1395        guard(&mut ops, lo, false);
1396        if signed {
1397            ops.push(ArmOp::I32TruncF32S {
1398                rd: Reg::R0,
1399                sm: VfpReg::S0,
1400            });
1401        } else {
1402            ops.push(ArmOp::I32TruncF32U {
1403                rd: Reg::R0,
1404                sm: VfpReg::S0,
1405            });
1406        }
1407        ops
1408    }
1409
1410    #[test]
1411    fn trunc_f32_s_domain_guard_preserves_the_trap() {
1412        with_verification_context(|| {
1413            let validator = TranslationValidator::new();
1414            let result = validator
1415                .verify_trap_preservation(&WasmOp::I32TruncF32S, &shipped_trunc_f32_guard(true))
1416                .unwrap();
1417            assert_eq!(result, ValidationResult::Verified);
1418        });
1419    }
1420
1421    #[test]
1422    fn trunc_f32_u_domain_guard_preserves_the_trap() {
1423        with_verification_context(|| {
1424            let validator = TranslationValidator::new();
1425            let result = validator
1426                .verify_trap_preservation(&WasmOp::I32TruncF32U, &shipped_trunc_f32_guard(false))
1427                .unwrap();
1428            assert_eq!(result, ValidationResult::Verified);
1429        });
1430    }
1431
1432    /// RED-FIRST for the #709 class: the bare saturating VCVT (guards
1433    /// stripped) never traps — rejected with a counterexample.
1434    #[test]
1435    fn trunc_f32_without_domain_guard_is_rejected() {
1436        use synth_synthesis::rules::VfpReg;
1437        with_verification_context(|| {
1438            let validator = TranslationValidator::new();
1439            let arm_ops = [ArmOp::I32TruncF32S {
1440                rd: Reg::R0,
1441                sm: VfpReg::S0,
1442            }];
1443            let result = validator
1444                .verify_trap_preservation(&WasmOp::I32TruncF32S, &arm_ops)
1445                .unwrap();
1446            assert!(
1447                matches!(result, ValidationResult::Invalid { .. }),
1448                "guard-stripped trunc must be Invalid, got {result:?}"
1449            );
1450        });
1451    }
1452
1453    /// Half a domain guard (upper bound only) drops the lower-bound trap.
1454    #[test]
1455    fn trunc_f32_with_only_upper_guard_is_rejected() {
1456        with_verification_context(|| {
1457            let validator = TranslationValidator::new();
1458            let mut arm_ops = shipped_trunc_f32_guard(true);
1459            // Strip the second (lower-bound) guard: 5 ops per guard block.
1460            arm_ops.drain(5..10);
1461            let result = validator
1462                .verify_trap_preservation(&WasmOp::I32TruncF32S, &arm_ops)
1463                .unwrap();
1464            assert!(
1465                matches!(result, ValidationResult::Invalid { .. }),
1466                "upper-only trunc guard must be Invalid, got {result:?}"
1467            );
1468        });
1469    }
1470
1471    // --- call_indirect (LIVE at the pseudo-op guard level, #642/#664/#676) ---
1472
1473    fn call_indirect_pseudo(
1474        table_size: u32,
1475        null_check: bool,
1476        type_check: Option<(u32, u32)>,
1477    ) -> ArmOp {
1478        ArmOp::CallIndirect {
1479            rd: Reg::R0,
1480            type_idx: 0,
1481            table_index_reg: Reg::R0,
1482            table_size,
1483            table_byte_offset: 0,
1484            null_check,
1485            type_check,
1486        }
1487    }
1488
1489    #[test]
1490    fn call_indirect_matching_guards_preserve_the_traps() {
1491        with_verification_context(|| {
1492            let validator = TranslationValidator::new();
1493            // Homogeneous table, all slots initialized: bounds clause only.
1494            let result = validator
1495                .verify_call_indirect_trap_preservation(
1496                    &call_indirect_pseudo(8, false, None),
1497                    &CallIndirectSpec {
1498                        table_size: 8,
1499                        may_have_null_slot: false,
1500                        heterogeneous_expected_type: None,
1501                    },
1502                )
1503                .unwrap();
1504            assert_eq!(result, ValidationResult::Verified);
1505            // Null-slot table + runtime type check, guards resolved.
1506            let result = validator
1507                .verify_call_indirect_trap_preservation(
1508                    &call_indirect_pseudo(8, true, Some((3, 32))),
1509                    &CallIndirectSpec {
1510                        table_size: 8,
1511                        may_have_null_slot: true,
1512                        heterogeneous_expected_type: Some(3),
1513                    },
1514                )
1515                .unwrap();
1516            assert_eq!(result, ValidationResult::Verified);
1517        });
1518    }
1519
1520    /// RED-FIRST for the #664 class: table has uninitialized slots but the
1521    /// selector resolved `null_check: false` — the null trap is dropped.
1522    #[test]
1523    fn call_indirect_dropped_null_check_is_rejected() {
1524        with_verification_context(|| {
1525            let validator = TranslationValidator::new();
1526            let result = validator
1527                .verify_call_indirect_trap_preservation(
1528                    &call_indirect_pseudo(8, false, None),
1529                    &CallIndirectSpec {
1530                        table_size: 8,
1531                        may_have_null_slot: true,
1532                        heterogeneous_expected_type: None,
1533                    },
1534                )
1535                .unwrap();
1536            assert!(
1537                matches!(result, ValidationResult::Invalid { .. }),
1538                "dropped null check must be Invalid, got {result:?}"
1539            );
1540        });
1541    }
1542
1543    /// RED-FIRST for the #642 class: the selector resolved the WRONG table
1544    /// size — indices in the gap escape the bounds trap.
1545    #[test]
1546    fn call_indirect_wrong_table_size_is_rejected() {
1547        with_verification_context(|| {
1548            let validator = TranslationValidator::new();
1549            let result = validator
1550                .verify_call_indirect_trap_preservation(
1551                    &call_indirect_pseudo(16, false, None),
1552                    &CallIndirectSpec {
1553                        table_size: 8,
1554                        may_have_null_slot: false,
1555                        heterogeneous_expected_type: None,
1556                    },
1557                )
1558                .unwrap();
1559            assert!(
1560                matches!(result, ValidationResult::Invalid { .. }),
1561                "wrong bounds size must be Invalid, got {result:?}"
1562            );
1563        });
1564    }
1565
1566    /// RED-FIRST for the #676 class: heterogeneous table but the runtime
1567    /// type check was dropped.
1568    #[test]
1569    fn call_indirect_dropped_type_check_is_rejected() {
1570        with_verification_context(|| {
1571            let validator = TranslationValidator::new();
1572            let result = validator
1573                .verify_call_indirect_trap_preservation(
1574                    &call_indirect_pseudo(8, true, None),
1575                    &CallIndirectSpec {
1576                        table_size: 8,
1577                        may_have_null_slot: true,
1578                        heterogeneous_expected_type: Some(3),
1579                    },
1580                )
1581                .unwrap();
1582            assert!(
1583                matches!(result, ValidationResult::Invalid { .. }),
1584                "dropped type check must be Invalid, got {result:?}"
1585            );
1586        });
1587    }
1588
1589    // --- verify_rule routes partial ops through the trap VC (mandatory) ---
1590
1591    #[test]
1592    fn verify_rule_routes_partial_ops_through_the_trap_gate() {
1593        with_verification_context(|| {
1594            let validator = TranslationValidator::new();
1595            // A bare-UDIV rule: value-plausible, trap-dropping. verify_rule
1596            // must report Invalid (via the trap VC), not silently value-check.
1597            let rule = SynthesisRule {
1598                name: "i32.div_u → bare UDIV (trap-dropping)".into(),
1599                priority: 0,
1600                pattern: Pattern::WasmInstr(WasmOp::I32DivU),
1601                replacement: Replacement::ArmInstr(ArmOp::Udiv {
1602                    rd: Reg::R0,
1603                    rn: Reg::R0,
1604                    rm: Reg::R1,
1605                }),
1606                cost: Cost {
1607                    cycles: 1,
1608                    code_size: 4,
1609                    registers: 2,
1610                },
1611            };
1612            let result = validator.verify_rule(&rule).unwrap();
1613            assert!(
1614                matches!(result, ValidationResult::Invalid { .. }),
1615                "verify_rule must reject the trap-dropping div rule, got {result:?}"
1616            );
1617
1618            // The guarded shape goes green through the same entry point.
1619            let rule = SynthesisRule {
1620                name: "i32.div_u → guarded UDIV".into(),
1621                priority: 0,
1622                pattern: Pattern::WasmInstr(WasmOp::I32DivU),
1623                replacement: Replacement::ArmSequence(shipped_divu_guard()),
1624                cost: Cost {
1625                    cycles: 4,
1626                    code_size: 10,
1627                    registers: 2,
1628                },
1629            };
1630            assert_eq!(
1631                validator.verify_rule(&rule).unwrap(),
1632                ValidationResult::Verified
1633            );
1634        });
1635    }
1636
1637    #[test]
1638    fn trap_preservation_gate_rejects_non_div_ops() {
1639        with_verification_context(|| {
1640            let validator = TranslationValidator::new();
1641            let err = validator
1642                .verify_div_rem_trap_preservation(&WasmOp::I32Add, &[])
1643                .unwrap_err();
1644            assert!(matches!(err, VerificationError::UnsupportedOperation(_)));
1645            // i64 div/rem is a div op but this method models 32-bit only —
1646            // it must Err rather than build wrong-width terms.
1647            let err64 = validator
1648                .verify_div_rem_trap_preservation(&WasmOp::I64DivU, &[])
1649                .unwrap_err();
1650            assert!(matches!(err64, VerificationError::UnsupportedOperation(_)));
1651        });
1652    }
1653
1654    #[test]
1655    fn test_verify_add_correct() {
1656        with_verification_context(|| {
1657            let validator = TranslationValidator::new();
1658
1659            let rule = create_test_rule(
1660                WasmOp::I32Add,
1661                ArmOp::Add {
1662                    rd: Reg::R0,
1663                    rn: Reg::R0,
1664                    op2: Operand2::Reg(Reg::R1),
1665                },
1666            );
1667
1668            let result = validator.verify_rule(&rule).unwrap();
1669            assert_eq!(result, ValidationResult::Verified);
1670        });
1671    }
1672
1673    #[test]
1674    fn test_verify_sub_correct() {
1675        with_verification_context(|| {
1676            let validator = TranslationValidator::new();
1677
1678            let rule = create_test_rule(
1679                WasmOp::I32Sub,
1680                ArmOp::Sub {
1681                    rd: Reg::R0,
1682                    rn: Reg::R0,
1683                    op2: Operand2::Reg(Reg::R1),
1684                },
1685            );
1686
1687            let result = validator.verify_rule(&rule).unwrap();
1688            assert_eq!(result, ValidationResult::Verified);
1689        });
1690    }
1691
1692    #[test]
1693    fn test_verify_mul_correct() {
1694        with_verification_context(|| {
1695            let validator = TranslationValidator::new();
1696
1697            let rule = create_test_rule(
1698                WasmOp::I32Mul,
1699                ArmOp::Mul {
1700                    rd: Reg::R0,
1701                    rn: Reg::R0,
1702                    rm: Reg::R1,
1703                },
1704            );
1705
1706            let result = validator.verify_rule(&rule).unwrap();
1707            assert_eq!(result, ValidationResult::Verified);
1708        });
1709    }
1710
1711    #[test]
1712    fn test_verify_and_correct() {
1713        with_verification_context(|| {
1714            let validator = TranslationValidator::new();
1715
1716            let rule = create_test_rule(
1717                WasmOp::I32And,
1718                ArmOp::And {
1719                    rd: Reg::R0,
1720                    rn: Reg::R0,
1721                    op2: Operand2::Reg(Reg::R1),
1722                },
1723            );
1724
1725            let result = validator.verify_rule(&rule).unwrap();
1726            assert_eq!(result, ValidationResult::Verified);
1727        });
1728    }
1729
1730    #[test]
1731    fn test_verify_incorrect_rule() {
1732        with_verification_context(|| {
1733            let validator = TranslationValidator::new();
1734
1735            // INCORRECT rule: WASM i32.add -> ARM SUB (should find counterexample)
1736            let rule = create_test_rule(
1737                WasmOp::I32Add,
1738                ArmOp::Sub {
1739                    rd: Reg::R0,
1740                    rn: Reg::R0,
1741                    op2: Operand2::Reg(Reg::R1),
1742                },
1743            );
1744
1745            let result = validator.verify_rule(&rule).unwrap();
1746
1747            match result {
1748                ValidationResult::Invalid { counterexample } => {
1749                    assert!(!counterexample.is_empty());
1750                }
1751                _ => panic!("Expected counterexample but got: {:?}", result),
1752            }
1753        });
1754    }
1755
1756    #[test]
1757    fn test_verify_bitwise_ops() {
1758        with_verification_context(|| {
1759            let validator = TranslationValidator::new();
1760
1761            // Test OR
1762            let or_rule = create_test_rule(
1763                WasmOp::I32Or,
1764                ArmOp::Orr {
1765                    rd: Reg::R0,
1766                    rn: Reg::R0,
1767                    op2: Operand2::Reg(Reg::R1),
1768                },
1769            );
1770            assert_eq!(
1771                validator.verify_rule(&or_rule).unwrap(),
1772                ValidationResult::Verified
1773            );
1774
1775            // Test XOR
1776            let xor_rule = create_test_rule(
1777                WasmOp::I32Xor,
1778                ArmOp::Eor {
1779                    rd: Reg::R0,
1780                    rn: Reg::R0,
1781                    op2: Operand2::Reg(Reg::R1),
1782                },
1783            );
1784            assert_eq!(
1785                validator.verify_rule(&xor_rule).unwrap(),
1786                ValidationResult::Verified
1787            );
1788        });
1789    }
1790
1791    #[test]
1792    fn test_verify_shift_ops() {
1793        // Note: Shift operations require concrete immediate values in ARM
1794        // but use register operands in WASM. Verification requires
1795        // modeling the shift amount modulo operation.
1796        // TODO: Implement shift verification with proper modulo handling
1797    }
1798}