1use 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#[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#[derive(Debug, Clone, PartialEq)]
54pub enum ValidationResult {
55 Verified,
57
58 Invalid { counterexample: Vec<(String, i64)> },
60
61 Unknown { reason: String },
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct CallIndirectSpec {
71 pub table_size: u32,
73 pub may_have_null_slot: bool,
76 pub heterogeneous_expected_type: Option<u32>,
80}
81
82pub 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 pub fn new() -> Self {
100 Self {
101 wasm_encoder: WasmSemantics::new(),
102 arm_encoder: ArmSemantics::new(),
103 timeout_ms: 30000, }
105 }
106
107 pub fn set_timeout(&mut self, timeout_ms: u64) {
109 self.timeout_ms = timeout_ms;
110 }
111
112 pub fn verify_rule(&self, rule: &SynthesisRule) -> Result<ValidationResult, VerificationError> {
117 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 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 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 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::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 | WasmOp::I32TruncF64S
183 | WasmOp::I32TruncF64U
184 )
185 }
186
187 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 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 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 BV::from_i64(*value, 32)
214 } else {
215 BV::new_const(format!("input_{}", i), 32)
217 };
218 inputs.push(input);
219 }
220
221 let wasm_result = self.wasm_encoder.encode_op(wasm_op, &inputs);
223
224 let arm_result = self.encode_arm_sequence(arm_ops, &inputs)?;
226
227 solver.assert(&wasm_result.eq(&arm_result).not());
231
232 match solver.check() {
233 CheckOutcome::Unsat => {
234 Ok(ValidationResult::Verified)
236 }
237
238 CheckOutcome::Sat => {
239 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 Ok(ValidationResult::Unknown {
257 reason: format!("SMT solver returned unknown: {reason}"),
258 })
259 }
260 }
261 }
262
263 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 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(®, input.clone());
285 }
286
287 for arm_op in arm_ops {
289 self.arm_encoder.encode_op(arm_op, &mut state);
290 }
291
292 Ok(self.arm_encoder.extract_result(&state, &Reg::R0))
294 }
295
296 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 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 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 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 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 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, ÷nd, &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 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 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 let static_bytes = offset as u64 + access_size;
535 let wasm_trap = if static_bytes > u32::MAX as u64 {
536 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 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 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 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 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, ÷nd, &divisor);
624
625 let arm_trap =
629 Self::i64_arm_trap_from_fields(div_op, ÷nd, &divisor, elide_zero, elide_overflow);
630
631 Ok(Self::condition_verdict(&wasm_trap, &arm_trap))
632 }
633
634 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 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 let mut clauses: Vec<Bool> = Vec::new();
674 if !elide_zero {
675 clauses.push(div_by_zero);
676 }
677
678 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(&[÷nd.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 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 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 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(®, input.clone());
834 }
835 Ok(())
836 }
837
838 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 fn get_num_inputs(&self, wasm_op: &WasmOp) -> usize {
862 use WasmOp::*;
863 match wasm_op {
864 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 I32Clz | I32Ctz | I32Popcnt | I32Eqz => 1,
871
872 I32Const(_) => 0,
874
875 I32Load { .. } => 1, I32Store { .. } => 2, LocalGet(_) | GlobalGet(_) => 0,
881 LocalSet(_) | GlobalSet(_) | LocalTee(_) => 1,
882 Br(_) | BrIf(_) | Return => 0,
883
884 Drop => 1,
886 Select => 3, Nop | Unreachable | Block | Loop | If | Else | End => 0,
888
889 _ => 0,
891 }
892 }
893
894 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 #[test]
933 fn div_lowering_without_guard_is_rejected_as_trap_drop() {
934 with_verification_context(|| {
935 let validator = TranslationValidator::new();
936 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 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 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 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 #[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 #[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 #[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 #[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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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), },
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 #[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; let arm_ops = [
1487 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 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 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 #[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 #[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 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 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) } else {
1670 (4294967296.0_f64, -1.0_f64) };
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); guard(&mut ops, lo, false); 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 #[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 #[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 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 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 #[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 #[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 #[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 #[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 #[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 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 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 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 #[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 #[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 #[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 #[test]
2138 fn verify_rule_routes_partial_ops_through_the_trap_gate() {
2139 with_verification_context(|| {
2140 let validator = TranslationValidator::new();
2141 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 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 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 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 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 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 }
2344}