use crate::eval;
use crate::solver::{CheckResult, Solver};
use crate::term::{BoolTerm, BvTerm, Sort};
fn bx(t: BvTerm) -> Box<BvTerm> {
Box::new(t)
}
fn bb(t: BoolTerm) -> Box<BoolTerm> {
Box::new(t)
}
fn bool_true() -> BoolTerm {
let z = || {
bx(BvTerm::Const {
value: 0,
sort: Sort::new(8),
})
};
BoolTerm::Eq(z(), z())
}
fn bool_false() -> BoolTerm {
BoolTerm::Not(bb(bool_true()))
}
fn zero_like(t: &BvTerm) -> BvTerm {
let width = eval::bv_sort(t).map(|s| s.width).unwrap_or(8);
BvTerm::Const {
value: 0,
sort: Sort::new(width),
}
}
#[derive(Clone, Debug)]
pub struct DefineOrTrap {
pub value: BvTerm,
pub may_trap: BoolTerm,
}
#[derive(Clone, Copy, Debug)]
pub enum DivOp {
DivU,
DivS,
RemU,
RemS,
}
impl DivOp {
fn is_signed(self) -> bool {
matches!(self, DivOp::DivS | DivOp::RemS)
}
}
pub fn trap_div(op: DivOp, dividend: &BvTerm, divisor: &BvTerm, width: u32) -> BoolTerm {
let sort = Sort::new(width);
let zero = BvTerm::Const { value: 0, sort };
let div_by_zero = BoolTerm::Eq(bx(divisor.clone()), bx(zero));
if !op.is_signed() {
return div_by_zero;
}
let int_min = 1u128 << (width - 1);
let all_ones = if width >= 128 {
u128::MAX
} else {
(1u128 << width) - 1
};
let overflow = BoolTerm::And(
bb(BoolTerm::Eq(
bx(dividend.clone()),
bx(BvTerm::Const {
value: int_min,
sort,
}),
)),
bb(BoolTerm::Eq(
bx(divisor.clone()),
bx(BvTerm::Const {
value: all_ones,
sort,
}),
)),
);
BoolTerm::Or(bb(div_by_zero), bb(overflow))
}
pub fn trap_always() -> BoolTerm {
bool_true()
}
pub fn trap_mem_oob(addr: &BvTerm, size: &BvTerm, mem_bound: &BvTerm) -> BoolTerm {
let ext = |t: &BvTerm| {
bx(BvTerm::ZeroExt {
by: 1,
arg: bx(t.clone()),
})
};
let end = BvTerm::Add(ext(addr), ext(size));
BoolTerm::Ugt(bx(end), ext(mem_bound))
}
pub enum TypeTrap<'a> {
Runtime {
actual_type_id: &'a BvTerm,
expected_id: &'a BvTerm,
},
StaticallyDischarged,
}
pub struct CallIndirect<'a> {
pub index: &'a BvTerm,
pub table_size: &'a BvTerm,
pub slot_ptr: &'a BvTerm,
pub type_trap: TypeTrap<'a>,
}
pub fn trap_call_indirect(ci: &CallIndirect) -> BoolTerm {
let bounds = BoolTerm::Uge(bx(ci.index.clone()), bx(ci.table_size.clone()));
let null_slot = BoolTerm::Eq(bx(ci.slot_ptr.clone()), bx(zero_like(ci.slot_ptr)));
let type_clause = match &ci.type_trap {
TypeTrap::Runtime {
actual_type_id,
expected_id,
} => BoolTerm::Ne(bx((*actual_type_id).clone()), bx((*expected_id).clone())),
TypeTrap::StaticallyDischarged => bool_false(),
};
BoolTerm::Or(bb(BoolTerm::Or(bb(bounds), bb(null_slot))), bb(type_clause))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FpFmt {
F32,
F64,
}
impl FpFmt {
pub const fn total_bits(self) -> u32 {
match self {
FpFmt::F32 => 32,
FpFmt::F64 => 64,
}
}
pub const fn exp_bits(self) -> u32 {
match self {
FpFmt::F32 => 8,
FpFmt::F64 => 11,
}
}
pub const fn mant_bits(self) -> u32 {
match self {
FpFmt::F32 => 23,
FpFmt::F64 => 52,
}
}
const fn bias(self) -> u32 {
match self {
FpFmt::F32 => 127,
FpFmt::F64 => 1023,
}
}
const fn exp_all_ones(self) -> u128 {
(1u128 << self.exp_bits()) - 1
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IntTarget {
I32,
I64,
}
impl IntTarget {
pub const fn width(self) -> u32 {
match self {
IntTarget::I32 => 32,
IntTarget::I64 => 64,
}
}
}
fn fp_exp_field(bits: &BvTerm, fmt: FpFmt) -> BvTerm {
BvTerm::Extract {
hi: fmt.total_bits() - 2,
lo: fmt.mant_bits(),
arg: bx(bits.clone()),
}
}
fn fp_mant_field(bits: &BvTerm, fmt: FpFmt) -> BvTerm {
BvTerm::Extract {
hi: fmt.mant_bits() - 1,
lo: 0,
arg: bx(bits.clone()),
}
}
fn fp_sign_bit(bits: &BvTerm, fmt: FpFmt) -> BvTerm {
let hi = fmt.total_bits() - 1;
BvTerm::Extract {
hi,
lo: hi,
arg: bx(bits.clone()),
}
}
fn fp_magnitude(bits: &BvTerm, fmt: FpFmt) -> BvTerm {
BvTerm::Extract {
hi: fmt.total_bits() - 2,
lo: 0,
arg: bx(bits.clone()),
}
}
fn fp_exp_is_all_ones(bits: &BvTerm, fmt: FpFmt) -> BoolTerm {
BoolTerm::Eq(
bx(fp_exp_field(bits, fmt)),
bx(BvTerm::Const {
value: fmt.exp_all_ones(),
sort: Sort::new(fmt.exp_bits()),
}),
)
}
pub fn fp_is_nan(bits: &BvTerm, fmt: FpFmt) -> BoolTerm {
let mant_nonzero = BoolTerm::Ne(
bx(fp_mant_field(bits, fmt)),
bx(BvTerm::Const {
value: 0,
sort: Sort::new(fmt.mant_bits()),
}),
);
BoolTerm::And(bb(fp_exp_is_all_ones(bits, fmt)), bb(mant_nonzero))
}
pub fn fp_is_inf(bits: &BvTerm, fmt: FpFmt) -> BoolTerm {
let mant_zero = BoolTerm::Eq(
bx(fp_mant_field(bits, fmt)),
bx(BvTerm::Const {
value: 0,
sort: Sort::new(fmt.mant_bits()),
}),
);
BoolTerm::And(bb(fp_exp_is_all_ones(bits, fmt)), bb(mant_zero))
}
fn pow2_magnitude_pattern(fmt: FpFmt, k: u32) -> u128 {
((fmt.bias() + k) as u128) << fmt.mant_bits()
}
fn min_pattern_ge_pow2_plus_1(fmt: FpFmt, k: u32) -> u128 {
let p2 = pow2_magnitude_pattern(fmt, k);
if fmt.mant_bits() >= k {
p2 | (1u128 << (fmt.mant_bits() - k))
} else {
p2 + 1
}
}
pub fn fp_trunc_out_of_range(
bits: &BvTerm,
fmt: FpFmt,
target: IntTarget,
signed: bool,
) -> BoolTerm {
let mag_sort = Sort::new(fmt.total_bits() - 1);
let mag = fp_magnitude(bits, fmt);
let is_neg = BoolTerm::Eq(
bx(fp_sign_bit(bits, fmt)),
bx(BvTerm::Const {
value: 1,
sort: Sort::new(1),
}),
);
let finite = BoolTerm::Not(bb(fp_exp_is_all_ones(bits, fmt)));
let k = target.width() - u32::from(signed);
let pos_thresh = pow2_magnitude_pattern(fmt, k);
let neg_thresh = if signed {
min_pattern_ge_pow2_plus_1(fmt, target.width() - 1)
} else {
pow2_magnitude_pattern(fmt, 0) };
let uge_const = |t: BvTerm, value: u128| {
BoolTerm::Uge(
bx(t),
bx(BvTerm::Const {
value,
sort: mag_sort,
}),
)
};
let pos_oob = BoolTerm::And(
bb(BoolTerm::Not(bb(is_neg.clone()))),
bb(uge_const(mag.clone(), pos_thresh)),
);
let neg_oob = BoolTerm::And(bb(is_neg), bb(uge_const(mag, neg_thresh)));
BoolTerm::And(bb(finite), bb(BoolTerm::Or(bb(pos_oob), bb(neg_oob))))
}
pub fn trap_trunc(bits: &BvTerm, fmt: FpFmt, target: IntTarget, signed: bool) -> BoolTerm {
BoolTerm::Or(
bb(BoolTerm::Or(
bb(fp_is_nan(bits, fmt)),
bb(fp_is_inf(bits, fmt)),
)),
bb(fp_trunc_out_of_range(bits, fmt, target, signed)),
)
}
pub fn trap_any(conds: &[BoolTerm]) -> BoolTerm {
match conds.split_first() {
None => bool_false(),
Some((first, rest)) => rest
.iter()
.fold(first.clone(), |acc, c| BoolTerm::Or(bb(acc), bb(c.clone()))),
}
}
fn iff(a: &BoolTerm, b: &BoolTerm) -> BoolTerm {
let imp =
|x: &BoolTerm, y: &BoolTerm| BoolTerm::Or(bb(BoolTerm::Not(bb(x.clone()))), bb(y.clone()));
BoolTerm::And(bb(imp(a, b)), bb(imp(b, a)))
}
pub fn trap_condition_equivalence(orig_may_trap: &BoolTerm, opt_may_trap: &BoolTerm) -> BoolTerm {
iff(orig_may_trap, opt_may_trap)
}
pub fn trap_equivalence_vc(orig: &DefineOrTrap, opt: &DefineOrTrap) -> BoolTerm {
let trap_eq = iff(&orig.may_trap, &opt.may_trap);
let value_eq = BoolTerm::Eq(bx(orig.value.clone()), bx(opt.value.clone()));
let guarded_value = BoolTerm::Or(bb(orig.may_trap.clone()), bb(value_eq));
BoolTerm::And(bb(trap_eq), bb(guarded_value))
}
fn prove_valid(goal: BoolTerm) -> CheckResult {
let mut s = Solver::new();
s.assert(BoolTerm::Not(bb(goal)));
s.check()
}
pub fn prove_trap_equivalence(orig: &DefineOrTrap, opt: &DefineOrTrap) -> CheckResult {
prove_valid(trap_equivalence_vc(orig, opt))
}
pub fn prove_trap_condition_equivalence(
orig_may_trap: &BoolTerm,
opt_may_trap: &BoolTerm,
) -> CheckResult {
prove_valid(trap_condition_equivalence(orig_may_trap, opt_may_trap))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::eval::Env;
fn v(name: &str, w: u32) -> BvTerm {
BvTerm::Var {
name: name.into(),
sort: Sort::new(w),
}
}
fn c(value: u128, w: u32) -> BvTerm {
BvTerm::Const {
value,
sort: Sort::new(w),
}
}
fn env2(a: u128, b: u128) -> Env {
let mut e = Env::new();
e.insert("a".into(), a);
e.insert("b".into(), b);
e
}
#[test]
fn trap_div_matches_wasm_semantics() {
let (a, b) = (v("a", 8), v("b", 8));
for op in [DivOp::DivU, DivOp::DivS, DivOp::RemU, DivOp::RemS] {
let cond = trap_div(op, &a, &b, 8);
for av in 0u128..256 {
for bv in 0u128..256 {
let got = eval::eval_bool(&cond, &env2(av, bv)).unwrap();
let zero = bv == 0;
let overflow = op.is_signed() && av == 0x80 && bv == 0xFF;
assert_eq!(got, zero || overflow, "{op:?} a={av} b={bv}");
}
}
}
}
#[test]
fn trap_always_is_true() {
assert!(eval::eval_bool(&trap_always(), &Env::new()).unwrap());
}
#[test]
fn trap_mem_oob_matches_reference_and_is_wraparound_safe() {
let addr = v("a", 8);
let bound = v("b", 8);
let size = c(4, 8);
let cond = trap_mem_oob(&addr, &size, &bound);
for a in 0u128..256 {
for b in 0u128..256 {
let got = eval::eval_bool(&cond, &env2(a, b)).unwrap();
assert_eq!(got, a + 4 > b, "addr={a} bound={b}");
}
}
assert!(eval::eval_bool(&cond, &env2(254, 255)).unwrap());
}
#[test]
fn trap_call_indirect_covers_bounds_null_and_type() {
let index = v("a", 32);
let table_size = c(10, 32);
let slot = v("b", 32);
let actual = BvTerm::Var {
name: "t".into(),
sort: Sort::new(32),
};
let expected = c(7, 32);
let ci = CallIndirect {
index: &index,
table_size: &table_size,
slot_ptr: &slot,
type_trap: TypeTrap::Runtime {
actual_type_id: &actual,
expected_id: &expected,
},
};
let cond = trap_call_indirect(&ci);
let eval = |idx: u128, slotv: u128, t: u128| {
let mut e = Env::new();
e.insert("a".into(), idx);
e.insert("b".into(), slotv);
e.insert("t".into(), t);
eval::eval_bool(&cond, &e).unwrap()
};
assert!(eval(10, 1, 7), "index == size is out of bounds");
assert!(eval(3, 0, 7), "null slot traps");
assert!(eval(3, 1, 9), "type mismatch traps");
assert!(
!eval(3, 1, 7),
"in-bounds, non-null, matching type: no trap"
);
}
#[test]
fn statically_discharged_type_never_contributes_a_trap() {
let index = v("a", 32);
let table_size = c(10, 32);
let slot = v("b", 32);
let ci = CallIndirect {
index: &index,
table_size: &table_size,
slot_ptr: &slot,
type_trap: TypeTrap::StaticallyDischarged,
};
let cond = trap_call_indirect(&ci);
let mut e = Env::new();
e.insert("a".into(), 3);
e.insert("b".into(), 1);
assert!(!eval::eval_bool(&cond, &e).unwrap());
}
#[test]
fn trap_any_is_the_or_fold() {
assert!(!eval::eval_bool(&trap_any(&[]), &Env::new()).unwrap());
let a_zero = BoolTerm::Eq(Box::new(v("a", 8)), Box::new(c(0, 8)));
let b_zero = BoolTerm::Eq(Box::new(v("b", 8)), Box::new(c(0, 8)));
let any = trap_any(&[a_zero, b_zero]);
assert!(eval::eval_bool(&any, &env2(0, 5)).unwrap());
assert!(eval::eval_bool(&any, &env2(5, 0)).unwrap());
assert!(!eval::eval_bool(&any, &env2(5, 5)).unwrap());
}
fn fval(fmt: FpFmt, p: u128) -> f64 {
match fmt {
FpFmt::F32 => f32::from_bits(p as u32) as f64,
FpFmt::F64 => f64::from_bits(p as u64),
}
}
fn ref_out_of_range(x: f64, target: IntTarget, signed: bool) -> bool {
let t = x.trunc();
let n = target.width() as i32;
if signed {
!(t >= -(2f64.powi(n - 1)) && t < 2f64.powi(n - 1))
} else {
!(t >= 0.0 && t < 2f64.powi(n))
}
}
fn ref_trap_trunc(x: f64, target: IntTarget, signed: bool) -> bool {
!x.is_finite() || ref_out_of_range(x, target, signed)
}
#[test]
fn derived_threshold_constants_match_ieee_bit_patterns() {
for k in [0u32, 31, 32, 63, 64] {
assert_eq!(
pow2_magnitude_pattern(FpFmt::F32, k),
2f32.powi(k as i32).to_bits() as u128,
"f32 2^{k}"
);
assert_eq!(
pow2_magnitude_pattern(FpFmt::F64, k),
2f64.powi(k as i32).to_bits() as u128,
"f64 2^{k}"
);
}
assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 31), 0x4F00_0000);
assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 32), 0x4F80_0000);
assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 63), 0x5F00_0000);
assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 64), 0x5F80_0000);
assert_eq!(pow2_magnitude_pattern(FpFmt::F32, 0), 0x3F80_0000);
assert_eq!(
pow2_magnitude_pattern(FpFmt::F64, 31),
0x41E0_0000_0000_0000
);
assert_eq!(
pow2_magnitude_pattern(FpFmt::F64, 32),
0x41F0_0000_0000_0000
);
assert_eq!(
pow2_magnitude_pattern(FpFmt::F64, 63),
0x43E0_0000_0000_0000
);
assert_eq!(
pow2_magnitude_pattern(FpFmt::F64, 64),
0x43F0_0000_0000_0000
);
assert_eq!(pow2_magnitude_pattern(FpFmt::F64, 0), 0x3FF0_0000_0000_0000);
assert_eq!(min_pattern_ge_pow2_plus_1(FpFmt::F32, 31), 0x4F00_0001);
assert_eq!(min_pattern_ge_pow2_plus_1(FpFmt::F32, 63), 0x5F00_0001);
assert_eq!(
min_pattern_ge_pow2_plus_1(FpFmt::F64, 31),
0x41E0_0000_0020_0000
);
assert_eq!(
min_pattern_ge_pow2_plus_1(FpFmt::F64, 63),
0x43E0_0000_0000_0001
);
for (fmt, k, thresh) in [
(FpFmt::F32, 31u32, 0x4F00_0001u128),
(FpFmt::F32, 63, 0x5F00_0001),
(FpFmt::F64, 31, 0x41E0_0000_0020_0000),
(FpFmt::F64, 63, 0x43E0_0000_0000_0001),
] {
if k <= 52 {
let bound = 2f64.powi(k as i32) + 1.0;
assert!(fval(fmt, thresh) >= bound, "{fmt:?} 2^{k}+1 at threshold");
assert!(fval(fmt, thresh - 1) < bound, "{fmt:?} 2^{k}+1 one below");
} else {
let bound = (1u128 << k) + 1;
assert!(
fval(fmt, thresh) as u128 >= bound,
"{fmt:?} 2^{k}+1 at threshold"
);
assert!(
(fval(fmt, thresh - 1) as u128) < bound,
"{fmt:?} 2^{k}+1 one below"
);
}
}
}
#[test]
fn nan_inf_classifiers_match_ieee_reference() {
for fmt in [FpFmt::F32, FpFmt::F64] {
let f = v("f", fmt.total_bits());
let nan_t = fp_is_nan(&f, fmt);
let inf_t = fp_is_inf(&f, fmt);
let mut env = Env::new();
for p in structured_patterns(fmt) {
env.insert("f".into(), p);
let x = fval(fmt, p);
assert_eq!(
eval::eval_bool(&nan_t, &env).unwrap(),
x.is_nan(),
"is_nan {fmt:?} pattern {p:#x}"
);
assert_eq!(
eval::eval_bool(&inf_t, &env).unwrap(),
x.is_infinite(),
"is_inf {fmt:?} pattern {p:#x}"
);
}
}
}
fn structured_patterns(fmt: FpFmt) -> Vec<u128> {
let m = fmt.mant_bits();
let mant_max = (1u128 << m) - 1;
let mut mants = vec![
0,
1,
2,
3,
mant_max,
mant_max - 1,
mant_max - 2,
mant_max / 3,
mant_max / 2,
2 * (mant_max / 3),
];
for i in 2..m {
mants.push(1u128 << i);
}
let mut out = Vec::new();
for exp in 0..=fmt.exp_all_ones() {
for &mant in &mants {
for sign in [0u128, 1] {
out.push((sign << (fmt.total_bits() - 1)) | (exp << m) | mant);
}
}
}
out
}
fn sweep_trunc_variant(fmt: FpFmt, target: IntTarget, signed: bool) {
let f = v("f", fmt.total_bits());
let oor_t = fp_trunc_out_of_range(&f, fmt, target, signed);
let trap_t = trap_trunc(&f, fmt, target, signed);
let mut env = Env::new();
let mut check = |p: u128| {
env.insert("f".into(), p);
let x = fval(fmt, p);
assert_eq!(
eval::eval_bool(&oor_t, &env).unwrap(),
x.is_finite() && ref_out_of_range(x, target, signed),
"out_of_range {fmt:?}->{target:?} signed={signed} pattern {p:#x} value {x:e}"
);
assert_eq!(
eval::eval_bool(&trap_t, &env).unwrap(),
ref_trap_trunc(x, target, signed),
"trap_trunc {fmt:?}->{target:?} signed={signed} pattern {p:#x} value {x:e}"
);
};
for p in structured_patterns(fmt) {
check(p);
}
let k = target.width() - u32::from(signed);
let pos_thresh = pow2_magnitude_pattern(fmt, k);
let neg_thresh = if signed {
min_pattern_ge_pow2_plus_1(fmt, target.width() - 1)
} else {
pow2_magnitude_pattern(fmt, 0)
};
for thresh in [pos_thresh, neg_thresh] {
for mag in (thresh - 64)..=(thresh + 64) {
for sign in [0u128, 1] {
check((sign << (fmt.total_bits() - 1)) | mag);
}
}
}
}
#[test]
fn trunc_f32_to_i32_signed_matches_reference() {
sweep_trunc_variant(FpFmt::F32, IntTarget::I32, true);
}
#[test]
fn trunc_f32_to_i32_unsigned_matches_reference() {
sweep_trunc_variant(FpFmt::F32, IntTarget::I32, false);
}
#[test]
fn trunc_f32_to_i64_signed_matches_reference() {
sweep_trunc_variant(FpFmt::F32, IntTarget::I64, true);
}
#[test]
fn trunc_f32_to_i64_unsigned_matches_reference() {
sweep_trunc_variant(FpFmt::F32, IntTarget::I64, false);
}
#[test]
fn trunc_f64_to_i32_signed_matches_reference() {
sweep_trunc_variant(FpFmt::F64, IntTarget::I32, true);
}
#[test]
fn trunc_f64_to_i32_unsigned_matches_reference() {
sweep_trunc_variant(FpFmt::F64, IntTarget::I32, false);
}
#[test]
fn trunc_f64_to_i64_signed_matches_reference() {
sweep_trunc_variant(FpFmt::F64, IntTarget::I64, true);
}
#[test]
fn trunc_f64_to_i64_unsigned_matches_reference() {
sweep_trunc_variant(FpFmt::F64, IntTarget::I64, false);
}
#[test]
fn trunc_boundary_cases_synth_709() {
#[track_caller]
fn t(fmt: FpFmt, target: IntTarget, signed: bool, p: u128, want: bool, label: &str) {
let f = v("f", fmt.total_bits());
let term = trap_trunc(&f, fmt, target, signed);
let mut e = Env::new();
e.insert("f".into(), p);
assert_eq!(eval::eval_bool(&term, &e).unwrap(), want, "{label}");
}
let b32 = |x: f32| x.to_bits() as u128;
let b64 = |x: f64| x.to_bits() as u128;
use FpFmt::{F32, F64};
use IntTarget::{I32, I64};
t(
F32,
I32,
true,
b32(2f32.powi(31)),
true,
"f32→i32_s: 2^31 traps",
);
#[allow(clippy::approx_constant)]
{
assert_eq!((2_147_483_647f32).to_bits(), 0x4F00_0000);
}
t(
F32,
I32,
true,
b32(-(2f32.powi(31))),
false,
"f32→i32_s: -2^31 is in range",
);
t(
F32,
I32,
true,
b32(2_147_483_520.0), false,
"f32→i32_s: largest f32 below 2^31 converts",
);
t(
F32,
I32,
true,
0xCF00_0001, true,
"f32→i32_s: -(2^31+256) traps",
);
t(F32, I32, true, b32(f32::NAN), true, "f32→i32_s: NaN traps");
t(
F32,
I32,
true,
b32(f32::INFINITY),
true,
"f32→i32_s: +∞ traps",
);
t(
F32,
I32,
true,
b32(f32::NEG_INFINITY),
true,
"f32→i32_s: -∞ traps",
);
t(F32, I32, true, b32(0.5), false, "f32→i32_s: 0.5 → 0");
t(F32, I32, true, b32(-0.5), false, "f32→i32_s: -0.5 → 0");
t(F32, I32, false, b32(-1.0), true, "f32→i32_u: -1.0 traps");
t(F32, I32, false, b32(0.5), false, "f32→i32_u: 0.5 → 0");
t(F32, I32, false, b32(-0.5), false, "f32→i32_u: -0.5 → 0");
t(F32, I32, false, b32(-0.0), false, "f32→i32_u: -0.0 → 0");
t(
F32,
I32,
false,
b32(f32::from_bits(0xBF7F_FFFF)), false,
"f32→i32_u: -(1-ε) → 0",
);
t(
F32,
I32,
false,
b32(2f32.powi(32)),
true,
"f32→i32_u: 2^32 traps",
);
t(
F32,
I32,
false,
b32(4_294_967_040.0), false,
"f32→i32_u: largest f32 below 2^32 converts",
);
t(F32, I32, false, b32(f32::NAN), true, "f32→i32_u: NaN traps");
t(
F64,
I32,
true,
b64(2f64.powi(31)),
true,
"f64→i32_s: 2^31 traps",
);
t(
F64,
I32,
true,
b64(2_147_483_647.5),
false,
"f64→i32_s: 2^31-0.5 → 2^31-1",
);
t(
F64,
I32,
true,
b64(-(2f64.powi(31))),
false,
"f64→i32_s: -2^31 is in range",
);
t(
F64,
I32,
true,
b64(-2_147_483_648.5),
false,
"f64→i32_s: -(2^31+0.5) → -2^31",
);
t(
F64,
I32,
true,
b64(-2_147_483_649.0),
true,
"f64→i32_s: -(2^31+1) traps",
);
t(F64, I32, true, b64(f64::NAN), true, "f64→i32_s: NaN traps");
t(F64, I32, false, b64(-1.0), true, "f64→i32_u: -1.0 traps");
t(
F64,
I32,
false,
b64(-0.999_999_999),
false,
"f64→i32_u: just above -1 → 0",
);
t(
F64,
I32,
false,
b64(2f64.powi(32)),
true,
"f64→i32_u: 2^32 traps",
);
t(
F64,
I32,
false,
b64(4_294_967_295.5),
false,
"f64→i32_u: 2^32-0.5 → 2^32-1",
);
t(
F32,
I64,
true,
b32(2f32.powi(63)),
true,
"f32→i64_s: 2^63 traps",
);
t(
F32,
I64,
true,
b32(f32::from_bits(0x5EFF_FFFF)), false,
"f32→i64_s: largest f32 below 2^63 converts",
);
t(
F32,
I64,
true,
b32(-(2f32.powi(63))),
false,
"f32→i64_s: -2^63 is in range",
);
t(
F32,
I64,
true,
0xDF00_0001, true,
"f32→i64_s: below -2^63 traps",
);
t(
F32,
I64,
false,
b32(2f32.powi(64)),
true,
"f32→i64_u: 2^64 traps",
);
t(
F32,
I64,
false,
b32(f32::from_bits(0x5F7F_FFFF)), false,
"f32→i64_u: largest f32 below 2^64 converts",
);
t(F32, I64, false, b32(-1.0), true, "f32→i64_u: -1.0 traps");
t(
F64,
I64,
true,
b64(2f64.powi(63)),
true,
"f64→i64_s: 2^63 traps",
);
t(
F64,
I64,
true,
0x43DF_FFFF_FFFF_FFFF, false,
"f64→i64_s: largest f64 below 2^63 converts",
);
t(
F64,
I64,
true,
b64(-(2f64.powi(63))),
false,
"f64→i64_s: -2^63 is in range",
);
t(
F64,
I64,
true,
0xC3E0_0000_0000_0001, true,
"f64→i64_s: below -2^63 traps",
);
t(
F64,
I64,
false,
b64(2f64.powi(64)),
true,
"f64→i64_u: 2^64 traps",
);
t(
F64,
I64,
false,
0x43EF_FFFF_FFFF_FFFF, false,
"f64→i64_u: largest f64 below 2^64 converts",
);
t(F64, I64, false, b64(-1.0), true, "f64→i64_u: -1.0 traps");
t(
F64,
I64,
false,
0xBFEF_FFFF_FFFF_FFFF, false,
"f64→i64_u: -(1-ε) → 0",
);
}
#[test]
#[ignore = "exhaustive 2^32 sweep; run with --release --ignored"]
fn exhaustive_f32_to_i32_all_bit_patterns() {
let threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(8);
let chunk = (1u64 << 32).div_ceil(threads as u64);
std::thread::scope(|s| {
for tid in 0..threads {
s.spawn(move || {
let f = v("f", 32);
let signed_t = trap_trunc(&f, FpFmt::F32, IntTarget::I32, true);
let unsigned_t = trap_trunc(&f, FpFmt::F32, IntTarget::I32, false);
let mut env = Env::new();
let lo = tid as u64 * chunk;
let hi = ((tid as u64 + 1) * chunk).min(1u64 << 32);
for p in lo..hi {
env.insert("f".into(), p as u128);
let x = f32::from_bits(p as u32) as f64;
assert_eq!(
eval::eval_bool(&signed_t, &env).unwrap(),
ref_trap_trunc(x, IntTarget::I32, true),
"i32.trunc_f32_s pattern {p:#010x}"
);
assert_eq!(
eval::eval_bool(&unsigned_t, &env).unwrap(),
ref_trap_trunc(x, IntTarget::I32, false),
"i32.trunc_f32_u pattern {p:#010x}"
);
}
});
}
});
}
#[test]
fn dropped_trunc_trap_is_caught_and_preserved_lowering_proves() {
let f = v("f", 32);
let trap = trap_trunc(&f, FpFmt::F32, IntTarget::I32, true);
let orig = DefineOrTrap {
value: f.clone(),
may_trap: trap.clone(),
};
let dropped = DefineOrTrap {
value: f.clone(),
may_trap: bool_false(),
};
match prove_trap_equivalence(&orig, &dropped) {
CheckResult::Sat(m) => {
let p = m
.assignments
.iter()
.find(|(n, _)| n == "f")
.map(|(_, x)| *x)
.expect("model must assign f");
let x = f32::from_bits(p as u32) as f64;
assert!(
ref_trap_trunc(x, IntTarget::I32, true),
"counterexample {p:#010x} must be a genuinely trapping input"
);
}
other => panic!("dropped trunc trap must be Sat, got {other:?}"),
}
let preserved = DefineOrTrap {
value: f.clone(),
may_trap: trap,
};
match prove_trap_equivalence(&orig, &preserved) {
CheckResult::Unsat(cert) => cert.recheck().expect("trunc-trap cert re-checks"),
other => panic!("preserved trunc trap must be Unsat, got {other:?}"),
}
}
#[test]
fn preserved_div_lowering_proves_unsat() {
let (a, b) = (v("a", 8), v("b", 8));
let value = BvTerm::Udiv(Box::new(a.clone()), Box::new(b.clone()));
let d = |val: BvTerm, t: BoolTerm| DefineOrTrap {
value: val,
may_trap: t,
};
let orig = d(value.clone(), trap_div(DivOp::DivU, &a, &b, 8));
let opt = d(value, trap_div(DivOp::DivU, &a, &b, 8));
match prove_trap_equivalence(&orig, &opt) {
CheckResult::Unsat(cert) => cert.recheck().expect("trap-equiv cert re-checks"),
other => panic!("preserved lowering must be Unsat, got {other:?}"),
}
}
#[test]
fn dropped_trap_is_caught_with_counterexample() {
let (a, b) = (v("a", 8), v("b", 8));
let value = BvTerm::Udiv(Box::new(a.clone()), Box::new(b.clone()));
let orig = DefineOrTrap {
value: value.clone(),
may_trap: trap_div(DivOp::DivU, &a, &b, 8),
};
let opt = DefineOrTrap {
value,
may_trap: bool_false(),
};
match prove_trap_equivalence(&orig, &opt) {
CheckResult::Sat(m) => {
let b_val = m
.assignments
.iter()
.find(|(n, _)| n == "b")
.map(|(_, x)| *x);
assert_eq!(b_val, Some(0), "counterexample must set divisor to 0");
}
other => panic!("dropped trap must be Sat, got {other:?}"),
}
}
#[test]
fn dropped_bounds_check_caught_by_conjunct1_gate() {
let addr = v("a", 8);
let bound = v("b", 8);
let size = c(4, 8);
let orig_trap = trap_mem_oob(&addr, &size, &bound);
let opt_trap = bool_false(); match prove_trap_condition_equivalence(&orig_trap, &opt_trap) {
CheckResult::Sat(_) => {}
other => panic!("dropped bounds check must be Sat, got {other:?}"),
}
match prove_trap_condition_equivalence(&orig_trap, &orig_trap) {
CheckResult::Unsat(cert) => cert.recheck().expect("re-check"),
other => panic!("preserved bounds check must be Unsat, got {other:?}"),
}
}
}