use std::ops::{Add, Div, Mul, Rem, Sub};
use crate::ast::SourceLocation;
use crate::error::{JITError, SpannedJITError};
use syn::BinOp;
#[derive(Debug, Eq, PartialEq)]
pub enum TileBinaryOp {
Add,
Sub,
Mul,
Div,
CeilDiv,
TrueDiv,
Rem,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
Min,
Max,
BitAnd,
BitOr,
BitXor,
Shl,
Shr,
}
pub fn get_binary_op_from_op_str(op_str: &str) -> Result<TileBinaryOp, JITError> {
match op_str {
"add" => Ok(TileBinaryOp::Add),
"sub" => Ok(TileBinaryOp::Sub),
"mul" => Ok(TileBinaryOp::Mul),
"div" => Ok(TileBinaryOp::Div),
"ceil_div" => Ok(TileBinaryOp::CeilDiv),
"true_div" => Ok(TileBinaryOp::TrueDiv),
"rem" => Ok(TileBinaryOp::Rem),
"eq" => Ok(TileBinaryOp::Eq),
"ne" => Ok(TileBinaryOp::Ne),
"lt" => Ok(TileBinaryOp::Lt),
"le" => Ok(TileBinaryOp::Le),
"gt" => Ok(TileBinaryOp::Gt),
"ge" => Ok(TileBinaryOp::Ge),
"min" | "min_tile" => Ok(TileBinaryOp::Min),
"max" | "max_tile" => Ok(TileBinaryOp::Max),
"and" => Ok(TileBinaryOp::BitAnd),
"or" => Ok(TileBinaryOp::BitOr),
"xor" => Ok(TileBinaryOp::BitXor),
"shl" => Ok(TileBinaryOp::Shl),
"shr" => Ok(TileBinaryOp::Shr),
_ => SourceLocation::unknown()
.jit_error_result(&format!("unrecognized arithmetic operation `{op_str}`")),
}
}
pub fn get_tile_bop_from_rust_bop(rust_bin_op: &BinOp) -> Result<TileBinaryOp, JITError> {
match rust_bin_op {
BinOp::Add(_) => Ok(TileBinaryOp::Add),
BinOp::Sub(_) => Ok(TileBinaryOp::Sub),
BinOp::Mul(_) => Ok(TileBinaryOp::Mul),
BinOp::Div(_) => Ok(TileBinaryOp::Div),
BinOp::Rem(_) => Ok(TileBinaryOp::Rem),
BinOp::Eq(_) => Ok(TileBinaryOp::Eq),
BinOp::Ne(_) => Ok(TileBinaryOp::Ne),
BinOp::Lt(_) => Ok(TileBinaryOp::Lt),
BinOp::Le(_) => Ok(TileBinaryOp::Le),
BinOp::Gt(_) => Ok(TileBinaryOp::Gt),
BinOp::Ge(_) => Ok(TileBinaryOp::Ge),
BinOp::BitAnd(_) => Ok(TileBinaryOp::BitAnd),
BinOp::BitOr(_) => Ok(TileBinaryOp::BitOr),
BinOp::BitXor(_) => Ok(TileBinaryOp::BitXor),
BinOp::And(_) => Ok(TileBinaryOp::BitAnd),
BinOp::Or(_) => Ok(TileBinaryOp::BitOr),
BinOp::Shl(_) => Ok(TileBinaryOp::Shl),
BinOp::Shr(_) => Ok(TileBinaryOp::Shr),
_ => SourceLocation::unknown().jit_error_result("this binary operator is not supported"),
}
}
fn div_ceil_i64(lhs: i64, rhs: i64) -> i64 {
let quotient = lhs.saturating_div(rhs);
let remainder = lhs.checked_rem(rhs).unwrap_or(0);
if remainder == 0 {
quotient
} else if (lhs > 0) == (rhs > 0) {
quotient.saturating_add(1)
} else {
quotient
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Bounds<T: Copy + PartialEq> {
pub start: T, pub end: T, }
impl<T: Copy + PartialEq> Bounds<T> {
pub fn new(start: T, end: T) -> Bounds<T> {
Self { start, end }
}
pub fn exact(value: T) -> Bounds<T> {
Self {
start: value,
end: value,
}
}
pub fn is_exact(&self) -> bool {
self.end == self.start
}
}
impl Add for Bounds<i64> {
type Output = Bounds<i64>;
fn add(self, rhs: Bounds<i64>) -> Bounds<i64> {
let a = self;
let b = rhs;
let possible_bounds = vec![
a.start.saturating_add(b.start),
a.start.saturating_add(b.end),
a.end.saturating_add(b.start),
a.end.saturating_add(b.end),
];
let start = *possible_bounds
.iter()
.min()
.expect("Unexpected failed min op.");
let end = *possible_bounds
.iter()
.max()
.expect("Unexpected failed max op.");
Bounds::new(start, end)
}
}
impl Sub for Bounds<i64> {
type Output = Bounds<i64>;
fn sub(self, rhs: Bounds<i64>) -> Bounds<i64> {
let a = self;
let b = rhs;
let possible_bounds = vec![
a.start.saturating_sub(b.start),
a.start.saturating_sub(b.end),
a.end.saturating_sub(b.start),
a.end.saturating_sub(b.end),
];
let start = *possible_bounds
.iter()
.min()
.expect("Unexpected failed min op.");
let end = *possible_bounds
.iter()
.max()
.expect("Unexpected failed max op.");
Bounds::new(start, end)
}
}
impl Mul for Bounds<i64> {
type Output = Bounds<i64>;
fn mul(self, rhs: Bounds<i64>) -> Bounds<i64> {
let a = self;
let b = rhs;
let possible_bounds = vec![
a.start.saturating_mul(b.start),
a.start.saturating_mul(b.end),
a.end.saturating_mul(b.start),
a.end.saturating_mul(b.end),
];
let start = *possible_bounds
.iter()
.min()
.expect("Unexpected failed min op.");
let end = *possible_bounds
.iter()
.max()
.expect("Unexpected failed max op.");
Bounds::new(start, end)
}
}
impl Div for Bounds<i64> {
type Output = Bounds<i64>;
fn div(self, rhs: Bounds<i64>) -> Bounds<i64> {
let a = self;
let b = rhs;
match (b.start, b.end) {
(0, 0) => panic!("Division by zero"),
(_, 0) => panic!("Division by zero"),
(0, _) => panic!("Division by zero"),
_ => {
let possible_bounds = vec![
a.start.saturating_div(b.start),
a.start.saturating_div(b.end),
a.end.saturating_div(b.start),
a.end.saturating_div(b.end),
];
let start = *possible_bounds
.iter()
.min()
.expect("Unexpected failed min op.");
let end = *possible_bounds
.iter()
.max()
.expect("Unexpected failed max op.");
Bounds::new(start, end)
}
}
}
}
impl Rem for Bounds<i64> {
type Output = Bounds<i64>;
fn rem(self, rhs: Bounds<i64>) -> Bounds<i64> {
let a = self;
let b = rhs;
debug_assert!(
!(b.start <= 0 && 0 <= b.end),
"Rem bounds require a divisor interval that excludes zero"
);
if a.is_exact() && b.is_exact() {
return Bounds::exact(a.start.checked_rem(b.start).unwrap_or(0));
}
let m = (b.start.unsigned_abs().max(b.end.unsigned_abs()) - 1) as i64;
let start = if a.start >= 0 { 0 } else { a.start.max(-m) };
let end = if a.end <= 0 { 0 } else { a.end.min(m) };
Bounds::new(start, end)
}
}
pub fn bop_bounds<F: Fn(i64, i64) -> i64>(a: &Bounds<i64>, b: &Bounds<i64>, f: F) -> Bounds<i64> {
if a.is_exact() && b.is_exact() {
return Bounds::exact(f(a.start, b.start));
}
let possible_bounds = vec![
f(a.start, b.start),
f(a.start, b.end),
f(a.end, b.start),
f(a.end, b.end),
];
let start = *possible_bounds
.iter()
.min()
.expect("Unexpected failed min op.");
let end = *possible_bounds
.iter()
.max()
.expect("Unexpected failed max op.");
Bounds::new(start, end)
}
fn bitwise_bounds(op: &TileBinaryOp, a: &Bounds<i64>, b: &Bounds<i64>) -> Bounds<i64> {
let f = |a: i64, b: i64| match op {
TileBinaryOp::BitAnd => a & b,
TileBinaryOp::BitOr => a | b,
TileBinaryOp::BitXor => a ^ b,
_ => unreachable!(),
};
if a.is_exact() && b.is_exact() {
return Bounds::exact(f(a.start, b.start));
}
if a.start >= 0 && b.start >= 0 {
let ones_over = |v: i64| match 64 - v.leading_zeros() {
0 => 0,
n if n >= 63 => i64::MAX,
n => (1i64 << n) - 1,
};
return match op {
TileBinaryOp::BitAnd => Bounds::new(0, a.end.min(b.end)),
TileBinaryOp::BitOr => Bounds::new(a.start.max(b.start), ones_over(a.end | b.end)),
TileBinaryOp::BitXor => Bounds::new(0, ones_over(a.end | b.end)),
_ => unreachable!(),
};
}
let signed_bits = |v: i64| {
if v >= 0 {
65 - v.leading_zeros()
} else {
65 - v.leading_ones()
}
};
let k = [a.start, a.end, b.start, b.end]
.into_iter()
.map(signed_bits)
.max()
.expect("Unexpected failed max op.");
if k >= 64 {
Bounds::new(i64::MIN, i64::MAX)
} else {
Bounds::new(-(1i64 << (k - 1)), (1i64 << (k - 1)) - 1)
}
}
pub fn bounds_from_bop(op: &TileBinaryOp, a: &Bounds<i64>, b: &Bounds<i64>) -> Option<Bounds<i64>> {
match op {
TileBinaryOp::Shl | TileBinaryOp::Shr => None,
TileBinaryOp::CeilDiv | TileBinaryOp::Div | TileBinaryOp::TrueDiv | TileBinaryOp::Rem => {
if b.start <= 0 && 0 <= b.end {
None
} else {
Some(match op {
TileBinaryOp::Div | TileBinaryOp::TrueDiv => *a / *b,
TileBinaryOp::CeilDiv => bop_bounds(a, b, div_ceil_i64),
TileBinaryOp::Rem => *a % *b,
_ => unreachable!(),
})
}
}
_ => Some(match op {
TileBinaryOp::Add => *a + *b,
TileBinaryOp::Sub => *a - *b,
TileBinaryOp::Mul => *a * *b,
TileBinaryOp::Eq => {
if a.is_exact() && b.is_exact() {
Bounds::exact((a.start == b.start) as i64)
} else if a.end < b.start || b.end < a.start {
Bounds::exact(0) } else {
Bounds::new(0, 1) }
}
TileBinaryOp::Ne => {
if a.is_exact() && b.is_exact() {
Bounds::exact((a.start != b.start) as i64)
} else if a.end < b.start || b.end < a.start {
Bounds::exact(1) } else {
Bounds::new(0, 1) }
}
TileBinaryOp::Lt => bop_bounds(a, b, |a, b| (a < b) as i64),
TileBinaryOp::Le => bop_bounds(a, b, |a, b| (a <= b) as i64),
TileBinaryOp::Gt => bop_bounds(a, b, |a, b| (a > b) as i64),
TileBinaryOp::Ge => bop_bounds(a, b, |a, b| (a >= b) as i64),
TileBinaryOp::Min => bop_bounds(a, b, |a, b| a.min(b)),
TileBinaryOp::Max => bop_bounds(a, b, |a, b| a.max(b)),
TileBinaryOp::BitAnd | TileBinaryOp::BitOr | TileBinaryOp::BitXor => {
bitwise_bounds(op, a, b)
}
_ => unreachable!(),
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bnd(start: i64, end: i64) -> Bounds<i64> {
Bounds::new(start, end)
}
#[test]
fn div_rejects_zero_at_an_endpoint() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Div, &bnd(1, 10), &bnd(0, 5)),
None
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Div, &bnd(1, 10), &bnd(-5, 0)),
None
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Div, &bnd(1, 10), &bnd(0, 0)),
None
);
}
#[test]
fn div_rejects_zero_in_the_interior() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Div, &bnd(100, 100), &bnd(-1, 2)),
None
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::CeilDiv, &bnd(1, 10), &bnd(-1, 3)),
None
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::TrueDiv, &bnd(1, 10), &bnd(-2, 4)),
None
);
}
#[test]
fn div_accepts_sign_consistent_divisor() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Div, &bnd(0, 10), &bnd(2, 2)),
Some(bnd(0, 5))
);
}
#[test]
fn rem_rejects_zero_divisor_instead_of_panicking() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Rem, &bnd(0, 10), &bnd(0, 3)),
None
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Rem, &bnd(0, 10), &bnd(-1, 2)),
None
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Rem, &bnd(0, 10), &bnd(0, 0)),
None
);
}
#[test]
fn rem_accepts_nonzero_divisor() {
assert!(bounds_from_bop(&TileBinaryOp::Rem, &bnd(0, 3), &bnd(3, 3)).is_some());
}
#[test]
fn rem_covers_the_full_residue_range() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Rem, &bnd(0, 3), &bnd(3, 3)),
Some(bnd(0, 2))
);
}
#[test]
fn rem_sign_follows_the_dividend() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Rem, &bnd(-7, -4), &bnd(3, 3)),
Some(bnd(-2, 0))
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Rem, &bnd(-3, 3), &bnd(2, 3)),
Some(bnd(-2, 2))
);
}
#[test]
fn rem_is_clamped_by_a_small_dividend() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Rem, &bnd(0, 1), &bnd(100, 100)),
Some(bnd(0, 1))
);
}
#[test]
fn rem_exact_operands_stay_exact() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Rem, &bnd(7, 7), &bnd(3, 3)),
Some(bnd(1, 1))
);
}
#[test]
fn rem_divisor_at_i64_min_covers_extreme_residues() {
assert_eq!(
bounds_from_bop(
&TileBinaryOp::Rem,
&bnd(i64::MAX - 1, i64::MAX),
&bnd(i64::MIN, i64::MIN)
),
Some(bnd(0, i64::MAX))
);
assert_eq!(
bounds_from_bop(
&TileBinaryOp::Rem,
&bnd(i64::MIN, i64::MIN + 1),
&bnd(i64::MIN, i64::MIN)
),
Some(bnd(i64::MIN + 1, 0))
);
}
#[test]
fn eq_disjoint_ranges_are_never_equal() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Eq, &bnd(0, 2), &bnd(5, 9)),
Some(bnd(0, 0))
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Ne, &bnd(0, 2), &bnd(5, 9)),
Some(bnd(1, 1))
);
}
#[test]
fn eq_overlapping_ranges_are_unknown() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Eq, &bnd(0, 5), &bnd(3, 9)),
Some(bnd(0, 1))
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Ne, &bnd(0, 5), &bnd(3, 9)),
Some(bnd(0, 1))
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Eq, &bnd(0, 3), &bnd(3, 5)),
Some(bnd(0, 1))
);
}
#[test]
fn eq_exact_operands_are_decided() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Eq, &bnd(4, 4), &bnd(4, 4)),
Some(bnd(1, 1))
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Eq, &bnd(4, 4), &bnd(5, 5)),
Some(bnd(0, 0))
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Ne, &bnd(4, 4), &bnd(4, 4)),
Some(bnd(0, 0))
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::Ne, &bnd(4, 4), &bnd(5, 5)),
Some(bnd(1, 1))
);
}
#[test]
fn xor_covers_interior_values() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::BitXor, &bnd(0, 2), &bnd(0, 2)),
Some(bnd(0, 3))
);
}
#[test]
fn and_covers_interior_values() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::BitAnd, &bnd(5, 8), &bnd(6, 7)),
Some(bnd(0, 7))
);
}
#[test]
fn or_is_bounded_below_by_its_operands() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::BitOr, &bnd(4, 6), &bnd(1, 3)),
Some(bnd(4, 7))
);
}
#[test]
fn bitwise_signed_operands_use_the_twos_complement_envelope() {
for op in [
TileBinaryOp::BitAnd,
TileBinaryOp::BitOr,
TileBinaryOp::BitXor,
] {
assert_eq!(
bounds_from_bop(&op, &bnd(-2, 2), &bnd(0, 3)),
Some(bnd(-4, 3))
);
}
}
#[test]
fn bitwise_exact_operands_stay_exact() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::BitXor, &bnd(6, 6), &bnd(3, 3)),
Some(bnd(5, 5))
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::BitAnd, &bnd(6, 6), &bnd(3, 3)),
Some(bnd(2, 2))
);
assert_eq!(
bounds_from_bop(&TileBinaryOp::BitOr, &bnd(6, 6), &bnd(3, 3)),
Some(bnd(7, 7))
);
}
#[test]
fn add_saturates_on_overflow() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Add, &bnd(i64::MAX, i64::MAX), &bnd(1, 1)),
Some(bnd(i64::MAX, i64::MAX))
);
}
#[test]
fn mul_saturates_on_overflow() {
assert_eq!(
bounds_from_bop(&TileBinaryOp::Mul, &bnd(0, i64::MAX), &bnd(2, 2)),
Some(bnd(0, i64::MAX))
);
}
#[test]
fn ceil_div_saturates_i64_min_over_negative_one() {
assert_eq!(
bounds_from_bop(
&TileBinaryOp::CeilDiv,
&bnd(i64::MIN, i64::MIN),
&bnd(-1, -1)
),
Some(bnd(i64::MAX, i64::MAX))
);
assert_eq!(
bounds_from_bop(
&TileBinaryOp::CeilDiv,
&bnd(i64::MIN, i64::MAX),
&bnd(-1, -1)
),
Some(bnd(-i64::MAX, i64::MAX))
);
}
}