1use crate::eval;
25use crate::solver::{CheckResult, Solver};
26use crate::term::{BoolTerm, BvTerm, Sort};
27
28fn bx(t: BvTerm) -> Box<BvTerm> {
29 Box::new(t)
30}
31fn bb(t: BoolTerm) -> Box<BoolTerm> {
32 Box::new(t)
33}
34
35fn bool_true() -> BoolTerm {
39 let z = || {
40 bx(BvTerm::Const {
41 value: 0,
42 sort: Sort::new(8),
43 })
44 };
45 BoolTerm::Eq(z(), z())
46}
47
48fn bool_false() -> BoolTerm {
50 BoolTerm::Not(bb(bool_true()))
51}
52
53fn zero_like(t: &BvTerm) -> BvTerm {
57 let width = eval::bv_sort(t).map(|s| s.width).unwrap_or(8);
58 BvTerm::Const {
59 value: 0,
60 sort: Sort::new(width),
61 }
62}
63
64#[derive(Clone, Debug)]
68pub struct DefineOrTrap {
69 pub value: BvTerm,
71 pub may_trap: BoolTerm,
73}
74
75#[derive(Clone, Copy, Debug)]
77pub enum DivOp {
78 DivU,
80 DivS,
82 RemU,
84 RemS,
86}
87
88impl DivOp {
89 fn traps_on_overflow(self) -> bool {
104 matches!(self, DivOp::DivS)
105 }
106}
107
108pub fn trap_div(op: DivOp, dividend: &BvTerm, divisor: &BvTerm, width: u32) -> BoolTerm {
113 let sort = Sort::new(width);
114 let zero = BvTerm::Const { value: 0, sort };
115 let div_by_zero = BoolTerm::Eq(bx(divisor.clone()), bx(zero));
116 if !op.traps_on_overflow() {
117 return div_by_zero;
118 }
119 let int_min = 1u128 << (width - 1);
121 let all_ones = if width >= 128 {
122 u128::MAX
123 } else {
124 (1u128 << width) - 1
125 };
126 let overflow = BoolTerm::And(
127 bb(BoolTerm::Eq(
128 bx(dividend.clone()),
129 bx(BvTerm::Const {
130 value: int_min,
131 sort,
132 }),
133 )),
134 bb(BoolTerm::Eq(
135 bx(divisor.clone()),
136 bx(BvTerm::Const {
137 value: all_ones,
138 sort,
139 }),
140 )),
141 );
142 BoolTerm::Or(bb(div_by_zero), bb(overflow))
143}
144
145pub fn trap_always() -> BoolTerm {
147 bool_true()
148}
149
150pub fn trap_mem_oob(addr: &BvTerm, size: &BvTerm, mem_bound: &BvTerm) -> BoolTerm {
156 let ext = |t: &BvTerm| {
157 bx(BvTerm::ZeroExt {
158 by: 1,
159 arg: bx(t.clone()),
160 })
161 };
162 let end = BvTerm::Add(ext(addr), ext(size));
163 BoolTerm::Ugt(bx(end), ext(mem_bound))
164}
165
166pub enum TypeTrap<'a> {
168 Runtime {
171 actual_type_id: &'a BvTerm,
173 expected_id: &'a BvTerm,
175 },
176 StaticallyDischarged,
180}
181
182pub struct CallIndirect<'a> {
184 pub index: &'a BvTerm,
186 pub table_size: &'a BvTerm,
188 pub slot_ptr: &'a BvTerm,
190 pub type_trap: TypeTrap<'a>,
192}
193
194pub fn trap_call_indirect(ci: &CallIndirect) -> BoolTerm {
197 let bounds = BoolTerm::Uge(bx(ci.index.clone()), bx(ci.table_size.clone()));
198 let null_slot = BoolTerm::Eq(bx(ci.slot_ptr.clone()), bx(zero_like(ci.slot_ptr)));
199 let type_clause = match &ci.type_trap {
200 TypeTrap::Runtime {
201 actual_type_id,
202 expected_id,
203 } => BoolTerm::Ne(bx((*actual_type_id).clone()), bx((*expected_id).clone())),
204 TypeTrap::StaticallyDischarged => bool_false(),
205 };
206 BoolTerm::Or(bb(BoolTerm::Or(bb(bounds), bb(null_slot))), bb(type_clause))
207}
208
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
223pub enum FpFmt {
224 F32,
226 F64,
228}
229
230impl FpFmt {
231 pub const fn total_bits(self) -> u32 {
233 match self {
234 FpFmt::F32 => 32,
235 FpFmt::F64 => 64,
236 }
237 }
238 pub const fn exp_bits(self) -> u32 {
240 match self {
241 FpFmt::F32 => 8,
242 FpFmt::F64 => 11,
243 }
244 }
245 pub const fn mant_bits(self) -> u32 {
247 match self {
248 FpFmt::F32 => 23,
249 FpFmt::F64 => 52,
250 }
251 }
252 const fn bias(self) -> u32 {
254 match self {
255 FpFmt::F32 => 127,
256 FpFmt::F64 => 1023,
257 }
258 }
259 const fn exp_all_ones(self) -> u128 {
261 (1u128 << self.exp_bits()) - 1
262 }
263}
264
265#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub enum IntTarget {
268 I32,
270 I64,
272}
273
274impl IntTarget {
275 pub const fn width(self) -> u32 {
277 match self {
278 IntTarget::I32 => 32,
279 IntTarget::I64 => 64,
280 }
281 }
282}
283
284fn fp_exp_field(bits: &BvTerm, fmt: FpFmt) -> BvTerm {
286 BvTerm::Extract {
287 hi: fmt.total_bits() - 2,
288 lo: fmt.mant_bits(),
289 arg: bx(bits.clone()),
290 }
291}
292
293fn fp_mant_field(bits: &BvTerm, fmt: FpFmt) -> BvTerm {
295 BvTerm::Extract {
296 hi: fmt.mant_bits() - 1,
297 lo: 0,
298 arg: bx(bits.clone()),
299 }
300}
301
302fn fp_sign_bit(bits: &BvTerm, fmt: FpFmt) -> BvTerm {
304 let hi = fmt.total_bits() - 1;
305 BvTerm::Extract {
306 hi,
307 lo: hi,
308 arg: bx(bits.clone()),
309 }
310}
311
312fn fp_magnitude(bits: &BvTerm, fmt: FpFmt) -> BvTerm {
315 BvTerm::Extract {
316 hi: fmt.total_bits() - 2,
317 lo: 0,
318 arg: bx(bits.clone()),
319 }
320}
321
322fn fp_exp_is_all_ones(bits: &BvTerm, fmt: FpFmt) -> BoolTerm {
324 BoolTerm::Eq(
325 bx(fp_exp_field(bits, fmt)),
326 bx(BvTerm::Const {
327 value: fmt.exp_all_ones(),
328 sort: Sort::new(fmt.exp_bits()),
329 }),
330 )
331}
332
333pub fn fp_is_nan(bits: &BvTerm, fmt: FpFmt) -> BoolTerm {
335 let mant_nonzero = BoolTerm::Ne(
336 bx(fp_mant_field(bits, fmt)),
337 bx(BvTerm::Const {
338 value: 0,
339 sort: Sort::new(fmt.mant_bits()),
340 }),
341 );
342 BoolTerm::And(bb(fp_exp_is_all_ones(bits, fmt)), bb(mant_nonzero))
343}
344
345pub fn fp_is_inf(bits: &BvTerm, fmt: FpFmt) -> BoolTerm {
347 let mant_zero = BoolTerm::Eq(
348 bx(fp_mant_field(bits, fmt)),
349 bx(BvTerm::Const {
350 value: 0,
351 sort: Sort::new(fmt.mant_bits()),
352 }),
353 );
354 BoolTerm::And(bb(fp_exp_is_all_ones(bits, fmt)), bb(mant_zero))
355}
356
357fn pow2_magnitude_pattern(fmt: FpFmt, k: u32) -> u128 {
360 ((fmt.bias() + k) as u128) << fmt.mant_bits()
361}
362
363fn min_pattern_ge_pow2_plus_1(fmt: FpFmt, k: u32) -> u128 {
371 let p2 = pow2_magnitude_pattern(fmt, k);
372 if fmt.mant_bits() >= k {
373 p2 | (1u128 << (fmt.mant_bits() - k))
374 } else {
375 p2 + 1
376 }
377}
378
379pub fn fp_trunc_out_of_range(
395 bits: &BvTerm,
396 fmt: FpFmt,
397 target: IntTarget,
398 signed: bool,
399) -> BoolTerm {
400 let mag_sort = Sort::new(fmt.total_bits() - 1);
401 let mag = fp_magnitude(bits, fmt);
402 let is_neg = BoolTerm::Eq(
403 bx(fp_sign_bit(bits, fmt)),
404 bx(BvTerm::Const {
405 value: 1,
406 sort: Sort::new(1),
407 }),
408 );
409 let finite = BoolTerm::Not(bb(fp_exp_is_all_ones(bits, fmt)));
410
411 let k = target.width() - u32::from(signed);
413 let pos_thresh = pow2_magnitude_pattern(fmt, k);
414 let neg_thresh = if signed {
416 min_pattern_ge_pow2_plus_1(fmt, target.width() - 1)
417 } else {
418 pow2_magnitude_pattern(fmt, 0) };
420
421 let uge_const = |t: BvTerm, value: u128| {
422 BoolTerm::Uge(
423 bx(t),
424 bx(BvTerm::Const {
425 value,
426 sort: mag_sort,
427 }),
428 )
429 };
430 let pos_oob = BoolTerm::And(
431 bb(BoolTerm::Not(bb(is_neg.clone()))),
432 bb(uge_const(mag.clone(), pos_thresh)),
433 );
434 let neg_oob = BoolTerm::And(bb(is_neg), bb(uge_const(mag, neg_thresh)));
435 BoolTerm::And(bb(finite), bb(BoolTerm::Or(bb(pos_oob), bb(neg_oob))))
436}
437
438pub fn trap_trunc(bits: &BvTerm, fmt: FpFmt, target: IntTarget, signed: bool) -> BoolTerm {
441 BoolTerm::Or(
442 bb(BoolTerm::Or(
443 bb(fp_is_nan(bits, fmt)),
444 bb(fp_is_inf(bits, fmt)),
445 )),
446 bb(fp_trunc_out_of_range(bits, fmt, target, signed)),
447 )
448}
449
450pub fn trap_any(conds: &[BoolTerm]) -> BoolTerm {
454 match conds.split_first() {
455 None => bool_false(),
456 Some((first, rest)) => rest
457 .iter()
458 .fold(first.clone(), |acc, c| BoolTerm::Or(bb(acc), bb(c.clone()))),
459 }
460}
461
462fn iff(a: &BoolTerm, b: &BoolTerm) -> BoolTerm {
465 let imp =
466 |x: &BoolTerm, y: &BoolTerm| BoolTerm::Or(bb(BoolTerm::Not(bb(x.clone()))), bb(y.clone()));
467 BoolTerm::And(bb(imp(a, b)), bb(imp(b, a)))
468}
469
470pub fn trap_condition_equivalence(orig_may_trap: &BoolTerm, opt_may_trap: &BoolTerm) -> BoolTerm {
476 iff(orig_may_trap, opt_may_trap)
477}
478
479pub fn trap_equivalence_vc(orig: &DefineOrTrap, opt: &DefineOrTrap) -> BoolTerm {
485 let trap_eq = iff(&orig.may_trap, &opt.may_trap);
486 let value_eq = BoolTerm::Eq(bx(orig.value.clone()), bx(opt.value.clone()));
488 let guarded_value = BoolTerm::Or(bb(orig.may_trap.clone()), bb(value_eq));
489 BoolTerm::And(bb(trap_eq), bb(guarded_value))
490}
491
492pub fn prove_trap_equivalence(orig: &DefineOrTrap, opt: &DefineOrTrap) -> CheckResult {
499 Solver::prove_valid(trap_equivalence_vc(orig, opt))
500}
501
502pub fn prove_trap_condition_equivalence(
506 orig_may_trap: &BoolTerm,
507 opt_may_trap: &BoolTerm,
508) -> CheckResult {
509 Solver::prove_valid(trap_condition_equivalence(orig_may_trap, opt_may_trap))
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use crate::eval::Env;
516
517 fn v(name: &str, w: u32) -> BvTerm {
518 BvTerm::Var {
519 name: name.into(),
520 sort: Sort::new(w),
521 }
522 }
523 fn c(value: u128, w: u32) -> BvTerm {
524 BvTerm::Const {
525 value,
526 sort: Sort::new(w),
527 }
528 }
529 fn env2(a: u128, b: u128) -> Env {
530 let mut e = Env::new();
531 e.insert("a".into(), a);
532 e.insert("b".into(), b);
533 e
534 }
535
536 fn wasm_div_traps(op: DivOp, x: u128, y: u128, w: u32) -> bool {
547 if y == 0 {
549 return true;
550 }
551 let to_signed = |v: u128| -> i128 {
552 let half = 1i128 << (w - 1);
553 let val = v as i128;
554 if val >= half { val - (1i128 << w) } else { val }
555 };
556 match op {
557 DivOp::DivU | DivOp::RemU => false,
559 DivOp::RemS => false,
562 DivOp::DivS => {
564 let q = to_signed(x) / to_signed(y);
565 let (lo, hi) = (-(1i128 << (w - 1)), (1i128 << (w - 1)) - 1);
566 q < lo || q > hi
567 }
568 }
569 }
570
571 #[test]
572 fn trap_div_matches_wasm_semantics() {
573 let w = 8u32;
574 let (a, b) = (v("a", w), v("b", w));
575 for op in [DivOp::DivU, DivOp::DivS, DivOp::RemU, DivOp::RemS] {
576 let cond = trap_div(op, &a, &b, w);
577 for av in 0u128..256 {
578 for bv in 0u128..256 {
579 let got = eval::eval_bool(&cond, &env2(av, bv)).unwrap();
580 let want = wasm_div_traps(op, av, bv, w);
581 assert_eq!(got, want, "{op:?} a={av:#04x} b={bv:#04x}");
582 }
583 }
584 }
585 }
586
587 #[test]
588 fn rem_s_does_not_trap_on_the_int_min_over_minus_one_pair() {
589 for w in [8u32, 32] {
594 let (a, b) = (v("a", w), v("b", w));
595 let int_min = 1u128 << (w - 1);
596 let minus_one = if w >= 128 {
597 u128::MAX
598 } else {
599 (1u128 << w) - 1
600 };
601 let at = |op: DivOp| {
602 let mut e = Env::new();
603 e.insert("a".into(), int_min);
604 e.insert("b".into(), minus_one);
605 eval::eval_bool(&trap_div(op, &a, &b, w), &e).unwrap()
606 };
607 assert!(at(DivOp::DivS), "div_s MUST trap on INT_MIN/-1 (w{w})");
608 assert!(!at(DivOp::RemS), "rem_s must NOT trap on INT_MIN/-1 (w{w})");
609 assert!(!at(DivOp::RemU), "rem_u must NOT trap on INT_MIN/-1 (w{w})");
610 for op in [DivOp::DivU, DivOp::DivS, DivOp::RemU, DivOp::RemS] {
612 let mut e = Env::new();
613 e.insert("a".into(), int_min);
614 e.insert("b".into(), 0);
615 assert!(
616 eval::eval_bool(&trap_div(op, &a, &b, w), &e).unwrap(),
617 "{op:?} must trap on divisor zero (w{w})"
618 );
619 }
620 }
621 }
622
623 #[test]
624 fn trap_always_is_true() {
625 assert!(eval::eval_bool(&trap_always(), &Env::new()).unwrap());
626 }
627
628 #[test]
629 fn trap_mem_oob_matches_reference_and_is_wraparound_safe() {
630 let addr = v("a", 8);
632 let bound = v("b", 8);
633 let size = c(4, 8);
634 let cond = trap_mem_oob(&addr, &size, &bound);
635 for a in 0u128..256 {
636 for b in 0u128..256 {
637 let got = eval::eval_bool(&cond, &env2(a, b)).unwrap();
638 assert_eq!(got, a + 4 > b, "addr={a} bound={b}");
640 }
641 }
642 assert!(eval::eval_bool(&cond, &env2(254, 255)).unwrap());
645 }
646
647 #[test]
648 fn trap_call_indirect_covers_bounds_null_and_type() {
649 let index = v("a", 32);
650 let table_size = c(10, 32);
651 let slot = v("b", 32);
652 let actual = BvTerm::Var {
654 name: "t".into(),
655 sort: Sort::new(32),
656 };
657 let expected = c(7, 32);
658 let ci = CallIndirect {
659 index: &index,
660 table_size: &table_size,
661 slot_ptr: &slot,
662 type_trap: TypeTrap::Runtime {
663 actual_type_id: &actual,
664 expected_id: &expected,
665 },
666 };
667 let cond = trap_call_indirect(&ci);
668 let eval = |idx: u128, slotv: u128, t: u128| {
669 let mut e = Env::new();
670 e.insert("a".into(), idx);
671 e.insert("b".into(), slotv);
672 e.insert("t".into(), t);
673 eval::eval_bool(&cond, &e).unwrap()
674 };
675 assert!(eval(10, 1, 7), "index == size is out of bounds");
676 assert!(eval(3, 0, 7), "null slot traps");
677 assert!(eval(3, 1, 9), "type mismatch traps");
678 assert!(
679 !eval(3, 1, 7),
680 "in-bounds, non-null, matching type: no trap"
681 );
682 }
683
684 #[test]
685 fn statically_discharged_type_never_contributes_a_trap() {
686 let index = v("a", 32);
687 let table_size = c(10, 32);
688 let slot = v("b", 32);
689 let ci = CallIndirect {
690 index: &index,
691 table_size: &table_size,
692 slot_ptr: &slot,
693 type_trap: TypeTrap::StaticallyDischarged,
694 };
695 let cond = trap_call_indirect(&ci);
696 let mut e = Env::new();
698 e.insert("a".into(), 3);
699 e.insert("b".into(), 1);
700 assert!(!eval::eval_bool(&cond, &e).unwrap());
701 }
702
703 #[test]
704 fn trap_any_is_the_or_fold() {
705 assert!(!eval::eval_bool(&trap_any(&[]), &Env::new()).unwrap());
706 let a_zero = BoolTerm::Eq(Box::new(v("a", 8)), Box::new(c(0, 8)));
707 let b_zero = BoolTerm::Eq(Box::new(v("b", 8)), Box::new(c(0, 8)));
708 let any = trap_any(&[a_zero, b_zero]);
709 assert!(eval::eval_bool(&any, &env2(0, 5)).unwrap());
710 assert!(eval::eval_bool(&any, &env2(5, 0)).unwrap());
711 assert!(!eval::eval_bool(&any, &env2(5, 5)).unwrap());
712 }
713
714 fn fval(fmt: FpFmt, p: u128) -> f64 {
728 match fmt {
729 FpFmt::F32 => f32::from_bits(p as u32) as f64,
730 FpFmt::F64 => f64::from_bits(p as u64),
731 }
732 }
733
734 fn ref_out_of_range(x: f64, target: IntTarget, signed: bool) -> bool {
737 let t = x.trunc();
738 let n = target.width() as i32;
739 if signed {
740 !(t >= -(2f64.powi(n - 1)) && t < 2f64.powi(n - 1))
741 } else {
742 !(t >= 0.0 && t < 2f64.powi(n))
743 }
744 }
745
746 fn ref_trap_trunc(x: f64, target: IntTarget, signed: bool) -> bool {
748 !x.is_finite() || ref_out_of_range(x, target, signed)
749 }
750
751 #[test]
756 fn derived_threshold_constants_match_ieee_bit_patterns() {
757 for k in [0u32, 31, 32, 63, 64] {
758 assert_eq!(
759 pow2_magnitude_pattern(FpFmt::F32, k),
760 2f32.powi(k as i32).to_bits() as u128,
761 "f32 2^{k}"
762 );
763 assert_eq!(
764 pow2_magnitude_pattern(FpFmt::F64, k),
765 2f64.powi(k as i32).to_bits() as u128,
766 "f64 2^{k}"
767 );
768 }
769 assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 31), 0x4F00_0000);
771 assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 32), 0x4F80_0000);
772 assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 63), 0x5F00_0000);
773 assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 64), 0x5F80_0000);
774 assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 0), 0x3F80_0000);
775 assert_eq!(
776 pow2_magnitude_pattern(FpFmt::F64, 31),
777 0x41E0_0000_0000_0000
778 );
779 assert_eq!(
780 pow2_magnitude_pattern(FpFmt::F64, 32),
781 0x41F0_0000_0000_0000
782 );
783 assert_eq!(
784 pow2_magnitude_pattern(FpFmt::F64, 63),
785 0x43E0_0000_0000_0000
786 );
787 assert_eq!(
788 pow2_magnitude_pattern(FpFmt::F64, 64),
789 0x43F0_0000_0000_0000
790 );
791 assert_eq!(pow2_magnitude_pattern(FpFmt::F64, 0), 0x3FF0_0000_0000_0000);
792 assert_eq!(min_pattern_ge_pow2_plus_1(FpFmt::F32, 31), 0x4F00_0001);
794 assert_eq!(min_pattern_ge_pow2_plus_1(FpFmt::F32, 63), 0x5F00_0001);
795 assert_eq!(
796 min_pattern_ge_pow2_plus_1(FpFmt::F64, 31),
797 0x41E0_0000_0020_0000
798 );
799 assert_eq!(
800 min_pattern_ge_pow2_plus_1(FpFmt::F64, 63),
801 0x43E0_0000_0000_0001
802 );
803 for (fmt, k, thresh) in [
806 (FpFmt::F32, 31u32, 0x4F00_0001u128),
807 (FpFmt::F32, 63, 0x5F00_0001),
808 (FpFmt::F64, 31, 0x41E0_0000_0020_0000),
809 (FpFmt::F64, 63, 0x43E0_0000_0000_0001),
810 ] {
811 if k <= 52 {
812 let bound = 2f64.powi(k as i32) + 1.0;
815 assert!(fval(fmt, thresh) >= bound, "{fmt:?} 2^{k}+1 at threshold");
816 assert!(fval(fmt, thresh - 1) < bound, "{fmt:?} 2^{k}+1 one below");
817 } else {
818 let bound = (1u128 << k) + 1;
822 assert!(
823 fval(fmt, thresh) as u128 >= bound,
824 "{fmt:?} 2^{k}+1 at threshold"
825 );
826 assert!(
827 (fval(fmt, thresh - 1) as u128) < bound,
828 "{fmt:?} 2^{k}+1 one below"
829 );
830 }
831 }
832 }
833
834 #[test]
837 fn nan_inf_classifiers_match_ieee_reference() {
838 for fmt in [FpFmt::F32, FpFmt::F64] {
839 let f = v("f", fmt.total_bits());
840 let nan_t = fp_is_nan(&f, fmt);
841 let inf_t = fp_is_inf(&f, fmt);
842 let mut env = Env::new();
843 for p in structured_patterns(fmt) {
844 env.insert("f".into(), p);
845 let x = fval(fmt, p);
846 assert_eq!(
847 eval::eval_bool(&nan_t, &env).unwrap(),
848 x.is_nan(),
849 "is_nan {fmt:?} pattern {p:#x}"
850 );
851 assert_eq!(
852 eval::eval_bool(&inf_t, &env).unwrap(),
853 x.is_infinite(),
854 "is_inf {fmt:?} pattern {p:#x}"
855 );
856 }
857 }
858 }
859
860 fn structured_patterns(fmt: FpFmt) -> Vec<u128> {
866 let m = fmt.mant_bits();
867 let mant_max = (1u128 << m) - 1;
868 let mut mants = vec![
869 0,
870 1,
871 2,
872 3,
873 mant_max,
874 mant_max - 1,
875 mant_max - 2,
876 mant_max / 3,
877 mant_max / 2,
878 2 * (mant_max / 3),
879 ];
880 for i in 2..m {
881 mants.push(1u128 << i);
882 }
883 let mut out = Vec::new();
884 for exp in 0..=fmt.exp_all_ones() {
885 for &mant in &mants {
886 for sign in [0u128, 1] {
887 out.push((sign << (fmt.total_bits() - 1)) | (exp << m) | mant);
888 }
889 }
890 }
891 out
892 }
893
894 fn sweep_trunc_variant(fmt: FpFmt, target: IntTarget, signed: bool) {
900 let f = v("f", fmt.total_bits());
901 let oor_t = fp_trunc_out_of_range(&f, fmt, target, signed);
902 let trap_t = trap_trunc(&f, fmt, target, signed);
903 let mut env = Env::new();
904 let mut check = |p: u128| {
905 env.insert("f".into(), p);
906 let x = fval(fmt, p);
907 assert_eq!(
908 eval::eval_bool(&oor_t, &env).unwrap(),
909 x.is_finite() && ref_out_of_range(x, target, signed),
910 "out_of_range {fmt:?}->{target:?} signed={signed} pattern {p:#x} value {x:e}"
911 );
912 assert_eq!(
913 eval::eval_bool(&trap_t, &env).unwrap(),
914 ref_trap_trunc(x, target, signed),
915 "trap_trunc {fmt:?}->{target:?} signed={signed} pattern {p:#x} value {x:e}"
916 );
917 };
918 for p in structured_patterns(fmt) {
919 check(p);
920 }
921 let k = target.width() - u32::from(signed);
922 let pos_thresh = pow2_magnitude_pattern(fmt, k);
923 let neg_thresh = if signed {
924 min_pattern_ge_pow2_plus_1(fmt, target.width() - 1)
925 } else {
926 pow2_magnitude_pattern(fmt, 0)
927 };
928 for thresh in [pos_thresh, neg_thresh] {
929 for mag in (thresh - 64)..=(thresh + 64) {
930 for sign in [0u128, 1] {
931 check((sign << (fmt.total_bits() - 1)) | mag);
932 }
933 }
934 }
935 }
936
937 #[test]
938 fn trunc_f32_to_i32_signed_matches_reference() {
939 sweep_trunc_variant(FpFmt::F32, IntTarget::I32, true);
940 }
941 #[test]
942 fn trunc_f32_to_i32_unsigned_matches_reference() {
943 sweep_trunc_variant(FpFmt::F32, IntTarget::I32, false);
944 }
945 #[test]
946 fn trunc_f32_to_i64_signed_matches_reference() {
947 sweep_trunc_variant(FpFmt::F32, IntTarget::I64, true);
948 }
949 #[test]
950 fn trunc_f32_to_i64_unsigned_matches_reference() {
951 sweep_trunc_variant(FpFmt::F32, IntTarget::I64, false);
952 }
953 #[test]
954 fn trunc_f64_to_i32_signed_matches_reference() {
955 sweep_trunc_variant(FpFmt::F64, IntTarget::I32, true);
956 }
957 #[test]
958 fn trunc_f64_to_i32_unsigned_matches_reference() {
959 sweep_trunc_variant(FpFmt::F64, IntTarget::I32, false);
960 }
961 #[test]
962 fn trunc_f64_to_i64_signed_matches_reference() {
963 sweep_trunc_variant(FpFmt::F64, IntTarget::I64, true);
964 }
965 #[test]
966 fn trunc_f64_to_i64_unsigned_matches_reference() {
967 sweep_trunc_variant(FpFmt::F64, IntTarget::I64, false);
968 }
969
970 #[test]
972 fn trunc_boundary_cases_synth_709() {
973 #[track_caller]
974 fn t(fmt: FpFmt, target: IntTarget, signed: bool, p: u128, want: bool, label: &str) {
975 let f = v("f", fmt.total_bits());
976 let term = trap_trunc(&f, fmt, target, signed);
977 let mut e = Env::new();
978 e.insert("f".into(), p);
979 assert_eq!(eval::eval_bool(&term, &e).unwrap(), want, "{label}");
980 }
981 let b32 = |x: f32| x.to_bits() as u128;
982 let b64 = |x: f64| x.to_bits() as u128;
983 use FpFmt::{F32, F64};
984 use IntTarget::{I32, I64};
985
986 t(
988 F32,
989 I32,
990 true,
991 b32(2f32.powi(31)),
992 true,
993 "f32→i32_s: 2^31 traps",
994 );
995 #[allow(clippy::approx_constant)]
996 {
997 assert_eq!((2_147_483_647f32).to_bits(), 0x4F00_0000);
999 }
1000 t(
1001 F32,
1002 I32,
1003 true,
1004 b32(-(2f32.powi(31))),
1005 false,
1006 "f32→i32_s: -2^31 is in range",
1007 );
1008 t(
1009 F32,
1010 I32,
1011 true,
1012 b32(2_147_483_520.0), false,
1014 "f32→i32_s: largest f32 below 2^31 converts",
1015 );
1016 t(
1017 F32,
1018 I32,
1019 true,
1020 0xCF00_0001, true,
1022 "f32→i32_s: -(2^31+256) traps",
1023 );
1024 t(F32, I32, true, b32(f32::NAN), true, "f32→i32_s: NaN traps");
1025 t(
1026 F32,
1027 I32,
1028 true,
1029 b32(f32::INFINITY),
1030 true,
1031 "f32→i32_s: +∞ traps",
1032 );
1033 t(
1034 F32,
1035 I32,
1036 true,
1037 b32(f32::NEG_INFINITY),
1038 true,
1039 "f32→i32_s: -∞ traps",
1040 );
1041 t(F32, I32, true, b32(0.5), false, "f32→i32_s: 0.5 → 0");
1042 t(F32, I32, true, b32(-0.5), false, "f32→i32_s: -0.5 → 0");
1043
1044 t(F32, I32, false, b32(-1.0), true, "f32→i32_u: -1.0 traps");
1046 t(F32, I32, false, b32(0.5), false, "f32→i32_u: 0.5 → 0");
1047 t(F32, I32, false, b32(-0.5), false, "f32→i32_u: -0.5 → 0");
1048 t(F32, I32, false, b32(-0.0), false, "f32→i32_u: -0.0 → 0");
1049 t(
1050 F32,
1051 I32,
1052 false,
1053 b32(f32::from_bits(0xBF7F_FFFF)), false,
1055 "f32→i32_u: -(1-ε) → 0",
1056 );
1057 t(
1058 F32,
1059 I32,
1060 false,
1061 b32(2f32.powi(32)),
1062 true,
1063 "f32→i32_u: 2^32 traps",
1064 );
1065 t(
1066 F32,
1067 I32,
1068 false,
1069 b32(4_294_967_040.0), false,
1071 "f32→i32_u: largest f32 below 2^32 converts",
1072 );
1073 t(F32, I32, false, b32(f32::NAN), true, "f32→i32_u: NaN traps");
1074
1075 t(
1077 F64,
1078 I32,
1079 true,
1080 b64(2f64.powi(31)),
1081 true,
1082 "f64→i32_s: 2^31 traps",
1083 );
1084 t(
1085 F64,
1086 I32,
1087 true,
1088 b64(2_147_483_647.5),
1089 false,
1090 "f64→i32_s: 2^31-0.5 → 2^31-1",
1091 );
1092 t(
1093 F64,
1094 I32,
1095 true,
1096 b64(-(2f64.powi(31))),
1097 false,
1098 "f64→i32_s: -2^31 is in range",
1099 );
1100 t(
1101 F64,
1102 I32,
1103 true,
1104 b64(-2_147_483_648.5),
1105 false,
1106 "f64→i32_s: -(2^31+0.5) → -2^31",
1107 );
1108 t(
1109 F64,
1110 I32,
1111 true,
1112 b64(-2_147_483_649.0),
1113 true,
1114 "f64→i32_s: -(2^31+1) traps",
1115 );
1116 t(F64, I32, true, b64(f64::NAN), true, "f64→i32_s: NaN traps");
1117
1118 t(F64, I32, false, b64(-1.0), true, "f64→i32_u: -1.0 traps");
1120 t(
1121 F64,
1122 I32,
1123 false,
1124 b64(-0.999_999_999),
1125 false,
1126 "f64→i32_u: just above -1 → 0",
1127 );
1128 t(
1129 F64,
1130 I32,
1131 false,
1132 b64(2f64.powi(32)),
1133 true,
1134 "f64→i32_u: 2^32 traps",
1135 );
1136 t(
1137 F64,
1138 I32,
1139 false,
1140 b64(4_294_967_295.5),
1141 false,
1142 "f64→i32_u: 2^32-0.5 → 2^32-1",
1143 );
1144
1145 t(
1147 F32,
1148 I64,
1149 true,
1150 b32(2f32.powi(63)),
1151 true,
1152 "f32→i64_s: 2^63 traps",
1153 );
1154 t(
1155 F32,
1156 I64,
1157 true,
1158 b32(f32::from_bits(0x5EFF_FFFF)), false,
1160 "f32→i64_s: largest f32 below 2^63 converts",
1161 );
1162 t(
1163 F32,
1164 I64,
1165 true,
1166 b32(-(2f32.powi(63))),
1167 false,
1168 "f32→i64_s: -2^63 is in range",
1169 );
1170 t(
1171 F32,
1172 I64,
1173 true,
1174 0xDF00_0001, true,
1176 "f32→i64_s: below -2^63 traps",
1177 );
1178
1179 t(
1181 F32,
1182 I64,
1183 false,
1184 b32(2f32.powi(64)),
1185 true,
1186 "f32→i64_u: 2^64 traps",
1187 );
1188 t(
1189 F32,
1190 I64,
1191 false,
1192 b32(f32::from_bits(0x5F7F_FFFF)), false,
1194 "f32→i64_u: largest f32 below 2^64 converts",
1195 );
1196 t(F32, I64, false, b32(-1.0), true, "f32→i64_u: -1.0 traps");
1197
1198 t(
1200 F64,
1201 I64,
1202 true,
1203 b64(2f64.powi(63)),
1204 true,
1205 "f64→i64_s: 2^63 traps",
1206 );
1207 t(
1208 F64,
1209 I64,
1210 true,
1211 0x43DF_FFFF_FFFF_FFFF, false,
1213 "f64→i64_s: largest f64 below 2^63 converts",
1214 );
1215 t(
1216 F64,
1217 I64,
1218 true,
1219 b64(-(2f64.powi(63))),
1220 false,
1221 "f64→i64_s: -2^63 is in range",
1222 );
1223 t(
1224 F64,
1225 I64,
1226 true,
1227 0xC3E0_0000_0000_0001, true,
1229 "f64→i64_s: below -2^63 traps",
1230 );
1231
1232 t(
1234 F64,
1235 I64,
1236 false,
1237 b64(2f64.powi(64)),
1238 true,
1239 "f64→i64_u: 2^64 traps",
1240 );
1241 t(
1242 F64,
1243 I64,
1244 false,
1245 0x43EF_FFFF_FFFF_FFFF, false,
1247 "f64→i64_u: largest f64 below 2^64 converts",
1248 );
1249 t(F64, I64, false, b64(-1.0), true, "f64→i64_u: -1.0 traps");
1250 t(
1251 F64,
1252 I64,
1253 false,
1254 0xBFEF_FFFF_FFFF_FFFF, false,
1256 "f64→i64_u: -(1-ε) → 0",
1257 );
1258 }
1259
1260 #[test]
1265 #[ignore = "exhaustive 2^32 sweep; run with --release --ignored"]
1266 fn exhaustive_f32_to_i32_all_bit_patterns() {
1267 let threads = std::thread::available_parallelism()
1268 .map(|n| n.get())
1269 .unwrap_or(8);
1270 let chunk = (1u64 << 32).div_ceil(threads as u64);
1271 std::thread::scope(|s| {
1272 for tid in 0..threads {
1273 s.spawn(move || {
1274 let f = v("f", 32);
1275 let signed_t = trap_trunc(&f, FpFmt::F32, IntTarget::I32, true);
1276 let unsigned_t = trap_trunc(&f, FpFmt::F32, IntTarget::I32, false);
1277 let mut env = Env::new();
1278 let lo = tid as u64 * chunk;
1279 let hi = ((tid as u64 + 1) * chunk).min(1u64 << 32);
1280 for p in lo..hi {
1281 env.insert("f".into(), p as u128);
1282 let x = f32::from_bits(p as u32) as f64;
1283 assert_eq!(
1284 eval::eval_bool(&signed_t, &env).unwrap(),
1285 ref_trap_trunc(x, IntTarget::I32, true),
1286 "i32.trunc_f32_s pattern {p:#010x}"
1287 );
1288 assert_eq!(
1289 eval::eval_bool(&unsigned_t, &env).unwrap(),
1290 ref_trap_trunc(x, IntTarget::I32, false),
1291 "i32.trunc_f32_u pattern {p:#010x}"
1292 );
1293 }
1294 });
1295 }
1296 });
1297 }
1298
1299 #[test]
1302 fn dropped_trunc_trap_is_caught_and_preserved_lowering_proves() {
1303 let f = v("f", 32);
1308 let trap = trap_trunc(&f, FpFmt::F32, IntTarget::I32, true);
1309 let orig = DefineOrTrap {
1310 value: f.clone(),
1311 may_trap: trap.clone(),
1312 };
1313 let dropped = DefineOrTrap {
1314 value: f.clone(),
1315 may_trap: bool_false(),
1316 };
1317 match prove_trap_equivalence(&orig, &dropped) {
1318 CheckResult::Sat(m) => {
1319 let p = m
1320 .assignments
1321 .iter()
1322 .find(|(n, _)| n == "f")
1323 .map(|(_, x)| *x)
1324 .expect("model must assign f");
1325 let x = f32::from_bits(p as u32) as f64;
1326 assert!(
1327 ref_trap_trunc(x, IntTarget::I32, true),
1328 "counterexample {p:#010x} must be a genuinely trapping input"
1329 );
1330 }
1331 other => panic!("dropped trunc trap must be Sat, got {other:?}"),
1332 }
1333 let preserved = DefineOrTrap {
1334 value: f.clone(),
1335 may_trap: trap,
1336 };
1337 match prove_trap_equivalence(&orig, &preserved) {
1338 CheckResult::Unsat(cert) => cert.recheck().expect("trunc-trap cert re-checks"),
1339 other => panic!("preserved trunc trap must be Unsat, got {other:?}"),
1340 }
1341 }
1342
1343 #[test]
1344 fn preserved_div_lowering_proves_unsat() {
1345 let (a, b) = (v("a", 8), v("b", 8));
1347 let value = BvTerm::Udiv(Box::new(a.clone()), Box::new(b.clone()));
1348 let d = |val: BvTerm, t: BoolTerm| DefineOrTrap {
1349 value: val,
1350 may_trap: t,
1351 };
1352 let orig = d(value.clone(), trap_div(DivOp::DivU, &a, &b, 8));
1353 let opt = d(value, trap_div(DivOp::DivU, &a, &b, 8));
1354 match prove_trap_equivalence(&orig, &opt) {
1355 CheckResult::Unsat(cert) => cert.recheck().expect("trap-equiv cert re-checks"),
1356 other => panic!("preserved lowering must be Unsat, got {other:?}"),
1357 }
1358 }
1359
1360 #[test]
1361 fn dropped_trap_is_caught_with_counterexample() {
1362 let (a, b) = (v("a", 8), v("b", 8));
1365 let value = BvTerm::Udiv(Box::new(a.clone()), Box::new(b.clone()));
1366 let orig = DefineOrTrap {
1367 value: value.clone(),
1368 may_trap: trap_div(DivOp::DivU, &a, &b, 8),
1369 };
1370 let opt = DefineOrTrap {
1371 value,
1372 may_trap: bool_false(),
1373 };
1374 match prove_trap_equivalence(&orig, &opt) {
1375 CheckResult::Sat(m) => {
1376 let b_val = m
1377 .assignments
1378 .iter()
1379 .find(|(n, _)| n == "b")
1380 .map(|(_, x)| *x);
1381 assert_eq!(b_val, Some(0), "counterexample must set divisor to 0");
1382 }
1383 other => panic!("dropped trap must be Sat, got {other:?}"),
1384 }
1385 }
1386
1387 #[test]
1388 fn dropped_bounds_check_caught_by_conjunct1_gate() {
1389 let addr = v("a", 8);
1392 let bound = v("b", 8);
1393 let size = c(4, 8);
1394 let orig_trap = trap_mem_oob(&addr, &size, &bound);
1395 let opt_trap = bool_false(); match prove_trap_condition_equivalence(&orig_trap, &opt_trap) {
1397 CheckResult::Sat(_) => {}
1398 other => panic!("dropped bounds check must be Sat, got {other:?}"),
1399 }
1400 match prove_trap_condition_equivalence(&orig_trap, &orig_trap) {
1402 CheckResult::Unsat(cert) => cert.recheck().expect("re-check"),
1403 other => panic!("preserved bounds check must be Unsat, got {other:?}"),
1404 }
1405 }
1406}