makepad/libs/relax/backend/tests/branch_not_compare.rs

106 lines
4.5 KiB
Rust

//! `if !(a <= b)` and friends: a compare that only feeds a negated branch condition branches on
//! the flags directly (no materialised bool), and NaN operands keep Rust's semantics: every
//! ordered compare is false on NaN so its negation is true; `!=` is true on NaN so its negation
//! is false.
#![cfg(target_arch = "x86_64")]
use relax_backend::{exec::Code, rir::*, x64};
use relax_backend::passes;
const CONDS: [Cond; 6] = [Cond::Lt, Cond::Le, Cond::Gt, Cond::Ge, Cond::Eq, Cond::Ne];
fn push(f: &mut Func, b: u32, i: Inst) {
f.blocks[b as usize].insts.push(i);
f.blocks[b as usize].pos.push(0);
}
/// f(a, b) = if NOT(a cc b) { 11 } else { 22 }, the NOT spelt `form`: 0 `== 0`, 1 `^ 1`, 2 none
/// (a plain branch), 3 `!= 0`.
fn build(w: bool, cc: Cond, form: u8, order: bool) -> Func {
let cls = if w { Cls::F64 } else { Cls::F32 };
let mut f = Func::new("branch_not".into(), 0);
let (a, b) = (f.vreg(cls), f.vreg(cls));
f.params = vec![a, b];
f.rets = vec![Cls::I];
let (b0, b1, b2) = (f.block(), f.block(), f.block());
let (c, d, k) = (f.vreg(Cls::I), f.vreg(Cls::I), f.vreg(Cls::I));
push(&mut f, b0, Inst::FCmp(cc, w, c, a, b));
let cond = match form {
0 => {
push(&mut f, b0, Inst::ICmpI(Cond::Eq, false, d, c, 0));
d
}
1 => {
push(&mut f, b0, Inst::IBinI(IOp::Xor, IntTy { bits: 8, signed: false }, d, c, 1));
d
}
3 => {
push(&mut f, b0, Inst::ICmpI(Cond::Ne, false, d, c, 0));
d
}
_ => c,
};
// block order decides which successor falls through
f.blocks[b0 as usize].term = if order { Term::Branch(cond, b1, b2) } else { Term::Branch(cond, b2, b1) };
push(&mut f, b1, Inst::Iconst(k, 11));
f.blocks[b1 as usize].term = Term::Ret(vec![k]);
let k2 = f.vreg(Cls::I);
push(&mut f, b2, Inst::Iconst(k2, 22));
f.blocks[b2 as usize].term = Term::Ret(vec![k2]);
f.verify().unwrap();
f
}
fn truth(cc: Cond, a: f64, b: f64) -> bool {
match cc {
Cond::Lt => a < b,
Cond::Le => a <= b,
Cond::Gt => a > b,
Cond::Ge => a >= b,
Cond::Eq => a == b,
Cond::Ne => a != b,
}
}
#[test]
fn negated_compare_branches_on_the_flags() {
let vals = [0.0, -0.0, 1.0, -1.0, 2.5, f64::INFINITY, f64::NEG_INFINITY, f64::NAN, -f64::NAN];
let options = relax_session::Options::default();
for w in [false, true] {
for cc in CONDS {
for form in 0..4u8 {
for order in [false, true] {
let mut f = build(w, cc, form, order);
passes::simplify(&options, &mut f, None, true, true);
if form != 2 {
// the negation is gone: the compare is the block's last instruction and
// the branch reads it
let b0 = &f.blocks[0];
assert!(matches!(b0.insts.last(), Some(Inst::FCmp(..))), "{w} {cc:?} {form}: {:?}", b0.insts);
assert!(b0.insts.iter().all(|i| !matches!(i, Inst::ICmpI(..) | Inst::IBinI(..))));
}
for regs in [false, true] {
let env = x64::Env { slots: SlotsPtr::NONE, tls_key: 0, tls_slow: 0, resume: 0, opt_regs: regs, syms: 0, sym_class: 0, near: false };
let c = x64::compile(&options, &f, &env).unwrap();
let code = Code::new(&c.code).unwrap();
for a in vals {
for b in vals {
let cmp = truth(cc, a, b);
let negated = form == 0 || form == 1;
let taken_b1 = if negated { !cmp } else { cmp } == order;
let want: u64 = if taken_b1 { 11 } else { 22 };
let got = if w {
let run: extern "sysv64" fn(f64, f64) -> u64 = unsafe { std::mem::transmute(code.ptr()) };
run(a, b)
} else {
let run: extern "sysv64" fn(f32, f32) -> u64 = unsafe { std::mem::transmute(code.ptr()) };
run(a as f32, b as f32)
};
assert_eq!(got, want, "w={w} {cc:?} form={form} order={order} regs={regs} a={a} b={b}");
}
}
}
}
}
}
}
}