use vyre_foundation::ir::BinOp;
use crate::{KernelBody, KernelDescriptor, KernelOpKind};
#[must_use]
pub fn canonicalize(desc: &KernelDescriptor) -> KernelDescriptor {
let mut out = desc.clone();
canonicalize_body(&mut out.body);
out
}
fn canonicalize_body(body: &mut KernelBody) {
for op in &mut body.ops {
if let KernelOpKind::BinOpKind(bo) = &op.kind {
if is_commutative(*bo) && op.operands.len() == 2 && op.operands[0] > op.operands[1] {
op.operands.swap(0, 1);
}
}
}
for child in &mut body.child_bodies {
canonicalize_body(child);
}
}
fn is_commutative(op: BinOp) -> bool {
matches!(
op,
BinOp::Add
| BinOp::Mul
| BinOp::BitAnd
| BinOp::BitOr
| BinOp::BitXor
| BinOp::Min
| BinOp::Max
| BinOp::Eq
| BinOp::Ne
| BinOp::WrappingAdd
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
BindingLayout, Dispatch, KernelBody, KernelDescriptor, KernelOp, KernelOpKind, LiteralValue,
};
fn empty_desc(ops: Vec<KernelOp>, literals: Vec<LiteralValue>) -> KernelDescriptor {
KernelDescriptor {
id: "k".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops,
child_bodies: vec![],
literals,
},
}
}
#[test]
fn add_with_swapped_operands_normalizes() {
let desc = empty_desc(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Add),
operands: vec![1, 0],
result: Some(2),
},
],
vec![LiteralValue::U32(3), LiteralValue::U32(4)],
);
let out = canonicalize(&desc);
assert_eq!(out.body.ops[2].operands, vec![0, 1]);
}
#[test]
fn sub_is_not_commuted() {
let desc = empty_desc(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Sub),
operands: vec![1, 0],
result: Some(2),
},
],
vec![LiteralValue::U32(3), LiteralValue::U32(4)],
);
let out = canonicalize(&desc);
assert_eq!(out.body.ops[2].operands, vec![1, 0]);
}
#[test]
fn already_canonical_unchanged() {
let desc = empty_desc(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Add),
operands: vec![0, 1],
result: Some(2),
},
],
vec![LiteralValue::U32(3), LiteralValue::U32(4)],
);
let out = canonicalize(&desc);
assert_eq!(out.body.ops[2].operands, vec![0, 1]);
}
#[test]
fn equal_operands_unchanged() {
let desc = empty_desc(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::BitAnd),
operands: vec![0, 0],
result: Some(1),
},
],
vec![LiteralValue::U32(7)],
);
let out = canonicalize(&desc);
assert_eq!(out.body.ops[1].operands, vec![0, 0]);
}
#[test]
fn enables_cse_to_merge_swapped_duplicates() {
let desc = empty_desc(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Add),
operands: vec![1, 0],
result: Some(2),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Add),
operands: vec![0, 1],
result: Some(3),
},
],
vec![LiteralValue::U32(3), LiteralValue::U32(4)],
);
let canon = canonicalize(&desc);
assert_eq!(canon.body.ops[2].operands, vec![0, 1]);
assert_eq!(canon.body.ops[3].operands, vec![0, 1]);
let after_cse = crate::rewrites::descriptor_cse(&canon);
let add_count = after_cse
.body
.ops
.iter()
.filter(|o| matches!(o.kind, KernelOpKind::BinOpKind(BinOp::Add)))
.count();
assert_eq!(add_count, 1);
}
#[test]
fn min_and_max_canonicalize() {
for op in [BinOp::Min, BinOp::Max] {
let desc = empty_desc(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(op),
operands: vec![1, 0],
result: Some(2),
},
],
vec![LiteralValue::U32(3), LiteralValue::U32(4)],
);
let out = canonicalize(&desc);
assert_eq!(
out.body.ops[2].operands,
vec![0, 1],
"{op:?} should canonicalize"
);
}
}
#[test]
fn shl_shr_lt_gt_not_canonicalized() {
for op in [BinOp::Shl, BinOp::Shr, BinOp::Lt, BinOp::Gt] {
let desc = empty_desc(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(op),
operands: vec![1, 0],
result: Some(2),
},
],
vec![LiteralValue::U32(3), LiteralValue::U32(4)],
);
let out = canonicalize(&desc);
assert_eq!(
out.body.ops[2].operands,
vec![1, 0],
"{op:?} must NOT be commuted"
);
}
}
#[test]
fn idempotent() {
let desc = empty_desc(
vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Mul),
operands: vec![1, 0],
result: Some(2),
},
],
vec![LiteralValue::U32(3), LiteralValue::U32(4)],
);
let once = canonicalize(&desc);
let twice = canonicalize(&once);
assert_eq!(once.body.ops, twice.body.ops);
}
#[test]
fn empty_kernel_is_noop() {
let desc = empty_desc(vec![], vec![]);
let out = canonicalize(&desc);
assert!(out.body.ops.is_empty());
}
#[test]
fn child_bodies_recurse() {
let desc = KernelDescriptor {
id: "k".into(),
bindings: BindingLayout { slots: vec![] },
dispatch: Dispatch::new(1, 1, 1),
body: KernelBody {
ops: vec![],
child_bodies: vec![KernelBody {
ops: vec![
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![0],
result: Some(0),
},
KernelOp {
kind: KernelOpKind::Literal,
operands: vec![1],
result: Some(1),
},
KernelOp {
kind: KernelOpKind::BinOpKind(BinOp::Add),
operands: vec![1, 0],
result: Some(2),
},
],
child_bodies: vec![],
literals: vec![LiteralValue::U32(3), LiteralValue::U32(4)],
}],
literals: vec![],
},
};
let out = canonicalize(&desc);
assert_eq!(out.body.child_bodies[0].ops[2].operands, vec![0, 1]);
}
}