use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Stage {
Jit,
Launch,
Device,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Atom {
Dim { param: usize, axis: usize },
Iv(u32),
TileBlockId(usize),
NumTileBlocks(usize),
ViewExtent { param: usize, axis: usize },
TileCount {
param: usize,
axis: usize,
tile: i32,
},
}
impl Atom {
pub fn stage(&self) -> Stage {
match self {
Atom::Dim { .. }
| Atom::NumTileBlocks(_)
| Atom::ViewExtent { .. }
| Atom::TileCount { .. } => Stage::Launch,
Atom::Iv(_) | Atom::TileBlockId(_) => Stage::Device,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct Term {
coeffs: BTreeMap<Atom, i64>,
constant: i64,
}
impl Term {
pub fn constant(c: i64) -> Self {
Term {
coeffs: BTreeMap::new(),
constant: c,
}
}
pub fn atom(a: Atom) -> Self {
let mut coeffs = BTreeMap::new();
coeffs.insert(a, 1);
Term {
coeffs,
constant: 0,
}
}
pub fn affine(a: Atom, scale: i64, offset: i64) -> Self {
if scale == 0 {
return Term::constant(offset);
}
let mut coeffs = BTreeMap::new();
coeffs.insert(a, scale);
Term {
coeffs,
constant: offset,
}
}
pub fn as_constant(&self) -> Option<i64> {
self.coeffs.is_empty().then_some(self.constant)
}
pub fn coeffs(&self) -> &BTreeMap<Atom, i64> {
&self.coeffs
}
pub fn constant_part(&self) -> i64 {
self.constant
}
pub fn stage(&self) -> Stage {
self.coeffs
.keys()
.map(Atom::stage)
.max()
.unwrap_or(Stage::Jit)
}
pub fn add(&self, other: &Term) -> Option<Term> {
let mut coeffs = self.coeffs.clone();
for (atom, &c) in &other.coeffs {
let entry = coeffs.entry(*atom).or_insert(0);
*entry = entry.checked_add(c)?;
if *entry == 0 {
coeffs.remove(atom);
}
}
Some(Term {
coeffs,
constant: self.constant.checked_add(other.constant)?,
})
}
pub fn neg(&self) -> Option<Term> {
let mut coeffs = BTreeMap::new();
for (atom, &c) in &self.coeffs {
coeffs.insert(*atom, c.checked_neg()?);
}
Some(Term {
coeffs,
constant: self.constant.checked_neg()?,
})
}
pub fn sub(&self, other: &Term) -> Option<Term> {
self.add(&other.neg()?)
}
pub fn mul_const(&self, k: i64) -> Option<Term> {
if k == 0 {
return Some(Term::constant(0));
}
let mut coeffs = BTreeMap::new();
for (atom, &c) in &self.coeffs {
coeffs.insert(*atom, c.checked_mul(k)?);
}
Some(Term {
coeffs,
constant: self.constant.checked_mul(k)?,
})
}
fn sign_canonical(&self) -> Term {
let leading_negative = match self.coeffs.iter().next() {
Some((_, &c)) => c < 0,
None => self.constant < 0,
};
if leading_negative {
self.neg().unwrap_or_else(|| self.clone())
} else {
self.clone()
}
}
pub fn as_single_affine(&self) -> Option<(Atom, i64, i64)> {
if self.coeffs.len() != 1 {
return None;
}
let (&atom, &scale) = self.coeffs.iter().next().unwrap();
Some((atom, scale, self.constant))
}
pub fn eval(&self, resolve_atom: &impl Fn(&Atom) -> Option<i64>) -> Option<i64> {
let mut acc = self.constant;
for (atom, &c) in &self.coeffs {
let v = resolve_atom(atom)?;
acc = acc.checked_add(c.checked_mul(v)?)?;
}
Some(acc)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Predicate {
Zero(Term),
Nonzero(Term),
Positive(Term),
DivisibleBy { term: Term, divisor: i64 },
}
impl Predicate {
pub fn eq(a: &Term, b: &Term) -> Option<Predicate> {
Some(Predicate::Zero(a.sub(b)?.sign_canonical()))
}
pub fn nonzero(term: Term) -> Predicate {
Predicate::Nonzero(term.sign_canonical())
}
pub fn lt(a: &Term, b: &Term) -> Option<Predicate> {
Some(Predicate::Positive(b.sub(a)?))
}
pub fn le(a: &Term, b: &Term) -> Option<Predicate> {
Some(Predicate::Positive(b.add(&Term::constant(1))?.sub(a)?))
}
pub fn divisible_by(term: Term, divisor: i64) -> Option<Predicate> {
if divisor < 1 {
return None;
}
let mut coeffs = BTreeMap::new();
for (atom, &c) in &term.coeffs {
let r = c.rem_euclid(divisor);
if r != 0 {
coeffs.insert(*atom, r);
}
}
let reduced = Term {
coeffs,
constant: term.constant.rem_euclid(divisor),
};
Some(Predicate::DivisibleBy {
term: reduced.sign_canonical(),
divisor,
})
}
pub fn stage(&self) -> Stage {
match self {
Predicate::Zero(t) | Predicate::Nonzero(t) | Predicate::Positive(t) => t.stage(),
Predicate::DivisibleBy { term, .. } => term.stage(),
}
}
pub fn eval(&self, resolve_atom: &impl Fn(&Atom) -> Option<i64>) -> Option<bool> {
match self {
Predicate::Zero(t) => Some(t.eval(resolve_atom)? == 0),
Predicate::Nonzero(t) => Some(t.eval(resolve_atom)? != 0),
Predicate::Positive(t) => Some(t.eval(resolve_atom)? > 0),
Predicate::DivisibleBy { term, divisor } => {
Some(term.eval(resolve_atom)?.rem_euclid(*divisor) == 0)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LaunchCheck {
pub predicate: Predicate,
pub cause: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn dim(param: usize, axis: usize) -> Atom {
Atom::Dim { param, axis }
}
#[test]
fn like_terms_combine_and_order_is_irrelevant() {
let a = Term::atom(dim(0, 0));
let b = Term::atom(dim(1, 0));
assert_eq!(a.add(&b).unwrap(), b.add(&a).unwrap());
assert_eq!(a.add(&a).unwrap(), a.mul_const(2).unwrap());
}
#[test]
fn like_terms_cancel_to_drop_zero_coefficients() {
let a = Term::atom(dim(0, 0));
let zero = a.sub(&a).unwrap();
assert_eq!(zero, Term::constant(0));
assert!(zero.coeffs().is_empty());
}
#[test]
fn confluence_two_build_orders_agree() {
let a = Term::atom(dim(0, 0));
let b = Term::atom(dim(1, 1));
let lhs = a
.mul_const(2)
.unwrap()
.add(&b)
.unwrap()
.add(&Term::constant(3))
.unwrap();
let rhs = b
.add(&Term::constant(3))
.unwrap()
.add(&a.mul_const(2).unwrap())
.unwrap();
assert_eq!(lhs, rhs);
}
#[test]
fn affine_consolidates_scale_var_offset() {
let a = dim(0, 0);
assert_eq!(
Term::affine(a, 3, 5),
Term::atom(a)
.mul_const(3)
.unwrap()
.add(&Term::constant(5))
.unwrap()
);
assert_eq!(Term::affine(a, 0, 7), Term::constant(7));
}
#[test]
fn as_single_affine_projects_only_single_var_forms() {
let a = dim(0, 0);
assert_eq!(Term::affine(a, 3, 5).as_single_affine(), Some((a, 3, 5)));
assert_eq!(Term::constant(7).as_single_affine(), None);
let two = Term::atom(dim(0, 0)).add(&Term::atom(dim(1, 0))).unwrap();
assert_eq!(two.as_single_affine(), None);
}
#[test]
fn stage_is_max_over_atoms() {
assert_eq!(Term::constant(4).stage(), Stage::Jit);
assert_eq!(Term::atom(dim(0, 0)).stage(), Stage::Launch);
let mixed = Term::atom(dim(0, 0)).add(&Term::atom(Atom::Iv(7))).unwrap();
assert_eq!(mixed.stage(), Stage::Device);
}
#[test]
fn overflow_bails_to_none() {
let big = Term::constant(i64::MAX);
assert!(big.add(&Term::constant(1)).is_none());
assert!(Term::atom(dim(0, 0))
.mul_const(2)
.unwrap()
.mul_const(i64::MAX)
.is_none());
}
#[test]
fn equality_is_symmetric_after_canonicalization() {
let a = Term::atom(dim(0, 0));
let b = Term::atom(dim(1, 0));
assert_eq!(
Predicate::eq(&a, &b).unwrap(),
Predicate::eq(&b, &a).unwrap()
);
}
#[test]
fn nonzero_is_sign_invariant() {
let a = Term::atom(dim(0, 0));
let neg_a = a.neg().unwrap();
assert_eq!(Predicate::nonzero(a), Predicate::nonzero(neg_a));
}
#[test]
fn divisibility_reduces_coefficients_mod_divisor() {
let a = dim(0, 0);
let p1 = Predicate::divisible_by(Term::affine(a, 6, 9), 3).unwrap();
let expected = Predicate::divisible_by(Term::constant(0), 3).unwrap();
assert_eq!(p1, expected);
assert!(Predicate::divisible_by(Term::constant(0), 0).is_none());
}
#[test]
fn eval_resolves_atoms_and_decides() {
let env = |atom: &Atom| match atom {
Atom::Dim { param: 0, axis: 0 } => Some(128),
_ => None,
};
let nonzero = Predicate::nonzero(Term::atom(dim(0, 0)));
assert_eq!(nonzero.eval(&env), Some(true));
let zero_env = |atom: &Atom| match atom {
Atom::Dim { param: 0, axis: 0 } => Some(0),
_ => None,
};
assert_eq!(nonzero.eval(&zero_env), Some(false));
let missing = Predicate::nonzero(Term::atom(dim(9, 9)));
assert_eq!(missing.eval(&env), None);
}
#[test]
fn special_register_stages() {
assert_eq!(Atom::TileBlockId(0).stage(), Stage::Device);
assert_eq!(Atom::NumTileBlocks(0).stage(), Stage::Launch);
assert_eq!(Term::atom(Atom::TileBlockId(0)).stage(), Stage::Device);
assert_eq!(Term::atom(Atom::NumTileBlocks(0)).stage(), Stage::Launch);
}
#[test]
fn lt_is_the_hardware_axiom_and_directional() {
let id = Term::atom(Atom::TileBlockId(0));
let n = Term::atom(Atom::NumTileBlocks(0));
let axiom = Predicate::lt(&id, &n).unwrap();
assert_ne!(axiom, Predicate::lt(&n, &id).unwrap());
assert_eq!(axiom.stage(), Stage::Device);
}
#[test]
fn lt_evaluates_strictly() {
let env = |atom: &Atom| match atom {
Atom::TileBlockId(0) => Some(3),
Atom::NumTileBlocks(0) => Some(4),
_ => None,
};
let lt = Predicate::lt(
&Term::atom(Atom::TileBlockId(0)),
&Term::atom(Atom::NumTileBlocks(0)),
)
.unwrap();
assert_eq!(lt.eval(&env), Some(true)); let eq_env = |atom: &Atom| match atom {
Atom::TileBlockId(0) => Some(4),
Atom::NumTileBlocks(0) => Some(4),
_ => None,
};
assert_eq!(lt.eval(&eq_env), Some(false)); }
#[test]
fn tile_count_evaluates_ceil_div_in_the_root_frame() {
let ta = Term::atom(Atom::TileCount {
param: 0,
axis: 1,
tile: 32,
});
let tb = Term::atom(Atom::TileCount {
param: 1,
axis: 0,
tile: 32,
});
let le = Predicate::le(&ta, &tb).unwrap();
assert_eq!(le.stage(), Stage::Launch);
let env = |a_extent: i64, b_extent: i64| {
move |atom: &Atom| match atom {
Atom::TileCount {
param: 0,
axis: 1,
tile: 32,
} => Some((a_extent + 31) / 32),
Atom::TileCount {
param: 1,
axis: 0,
tile: 32,
} => Some((b_extent + 31) / 32),
_ => None,
}
};
assert_eq!(le.eval(&env(128, 100)), Some(true));
assert_eq!(le.eval(&env(128, 96)), Some(false));
assert_ne!(
Term::atom(Atom::TileCount {
param: 0,
axis: 1,
tile: 32
}),
Term::atom(Atom::TileCount {
param: 0,
axis: 1,
tile: 16
})
);
}
#[test]
fn le_admits_equality_and_larger_and_rejects_smaller() {
let a = Term::atom(Atom::Dim { param: 0, axis: 1 });
let b = Term::atom(Atom::Dim { param: 1, axis: 0 });
let le = Predicate::le(&a, &b).unwrap();
let env = |x: i64, y: i64| {
move |atom: &Atom| match atom {
Atom::Dim { param: 0, axis: 1 } => Some(x),
Atom::Dim { param: 1, axis: 0 } => Some(y),
_ => None,
}
};
assert_eq!(le.eval(&env(64, 64)), Some(true)); assert_eq!(le.eval(&env(64, 256)), Some(true)); assert_eq!(le.eval(&env(128, 96)), Some(false)); let eq = Predicate::eq(&a, &b).unwrap();
assert_eq!(eq.eval(&env(64, 256)), Some(false));
let refl = Predicate::le(&a, &a).unwrap();
assert_eq!(refl.eval(&|_| None), Some(true));
}
#[test]
fn eval_divisibility() {
let env = |atom: &Atom| match atom {
Atom::Dim { param: 0, axis: 0 } => Some(256),
_ => None,
};
let div = Predicate::divisible_by(Term::atom(dim(0, 0)), 64).unwrap();
assert_eq!(div.eval(&env), Some(true));
let div2 = Predicate::divisible_by(Term::atom(dim(0, 0)), 100).unwrap();
assert_eq!(div2.eval(&env), Some(false));
}
}