use crate::array::Array;
use crate::error::Result;
use std::fmt;
use std::ops::{Add, Div, Mul, Neg, Sub};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BinOp {
Add,
Sub,
Mul,
Div,
}
impl BinOp {
fn symbol(self) -> &'static str {
match self {
BinOp::Add => "+",
BinOp::Sub => "-",
BinOp::Mul => "*",
BinOp::Div => "/",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnaryOp {
Neg,
Abs,
Sqrt,
Exp,
Ln,
}
impl UnaryOp {
fn name(self) -> &'static str {
match self {
UnaryOp::Neg => "neg",
UnaryOp::Abs => "abs",
UnaryOp::Sqrt => "sqrt",
UnaryOp::Exp => "exp",
UnaryOp::Ln => "ln",
}
}
}
pub enum ExprNode<T> {
Leaf(Array<T>),
Binary(BinOp, Box<ExprNode<T>>, Box<ExprNode<T>>),
ScalarRhs(BinOp, Box<ExprNode<T>>, T),
ScalarLhs(BinOp, T, Box<ExprNode<T>>),
Unary(UnaryOp, Box<ExprNode<T>>),
Fma(Box<ExprNode<T>>, Box<ExprNode<T>>, Box<ExprNode<T>>),
}
impl<T: Clone> Clone for ExprNode<T> {
fn clone(&self) -> Self {
match self {
ExprNode::Leaf(a) => ExprNode::Leaf(a.clone()),
ExprNode::Binary(op, l, r) => ExprNode::Binary(*op, l.clone(), r.clone()),
ExprNode::ScalarRhs(op, e, s) => ExprNode::ScalarRhs(*op, e.clone(), s.clone()),
ExprNode::ScalarLhs(op, s, e) => ExprNode::ScalarLhs(*op, s.clone(), e.clone()),
ExprNode::Unary(op, e) => ExprNode::Unary(*op, e.clone()),
ExprNode::Fma(a, b, c) => ExprNode::Fma(a.clone(), b.clone(), c.clone()),
}
}
}
impl<T: fmt::Debug + Clone> fmt::Debug for ExprNode<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ExprNode::Leaf(a) => write!(f, "Leaf(shape={:?})", a.shape()),
ExprNode::Binary(op, l, r) => {
write!(f, "({:?} {} {:?})", l, op.symbol(), r)
}
ExprNode::ScalarRhs(op, e, s) => write!(f, "({:?} {} {:?})", e, op.symbol(), s),
ExprNode::ScalarLhs(op, s, e) => write!(f, "({:?} {} {:?})", s, op.symbol(), e),
ExprNode::Unary(op, e) => write!(f, "{}({:?})", op.name(), e),
ExprNode::Fma(a, b, c) => write!(f, "fma({:?}, {:?}, {:?})", a, b, c),
}
}
}
impl<T> ExprNode<T> {
pub fn leaf_count(&self) -> usize {
match self {
ExprNode::Leaf(_) => 1,
ExprNode::Binary(_, l, r) => l.leaf_count() + r.leaf_count(),
ExprNode::ScalarRhs(_, e, _) | ExprNode::ScalarLhs(_, _, e) => e.leaf_count(),
ExprNode::Unary(_, e) => e.leaf_count(),
ExprNode::Fma(a, b, c) => a.leaf_count() + b.leaf_count() + c.leaf_count(),
}
}
pub fn depth(&self) -> usize {
match self {
ExprNode::Leaf(_) => 1,
ExprNode::Binary(_, l, r) => 1 + l.depth().max(r.depth()),
ExprNode::ScalarRhs(_, e, _) | ExprNode::ScalarLhs(_, _, e) => 1 + e.depth(),
ExprNode::Unary(_, e) => 1 + e.depth(),
ExprNode::Fma(a, b, c) => 1 + a.depth().max(b.depth()).max(c.depth()),
}
}
pub(super) fn collect_leaves<'a>(&'a self, out: &mut Vec<&'a Array<T>>) {
match self {
ExprNode::Leaf(a) => out.push(a),
ExprNode::Binary(_, l, r) => {
l.collect_leaves(out);
r.collect_leaves(out);
}
ExprNode::ScalarRhs(_, e, _) | ExprNode::ScalarLhs(_, _, e) => e.collect_leaves(out),
ExprNode::Unary(_, e) => e.collect_leaves(out),
ExprNode::Fma(a, b, c) => {
a.collect_leaves(out);
b.collect_leaves(out);
c.collect_leaves(out);
}
}
}
pub fn fuse_fma(self) -> Self {
match self {
ExprNode::Binary(BinOp::Add, lhs, rhs) => match *lhs {
ExprNode::Binary(BinOp::Mul, a, b) => ExprNode::Fma(
Box::new(a.fuse_fma()),
Box::new(b.fuse_fma()),
Box::new(rhs.fuse_fma()),
),
other => ExprNode::Binary(
BinOp::Add,
Box::new(other.fuse_fma()),
Box::new(rhs.fuse_fma()),
),
},
ExprNode::Binary(op, l, r) => {
ExprNode::Binary(op, Box::new(l.fuse_fma()), Box::new(r.fuse_fma()))
}
ExprNode::ScalarRhs(op, e, s) => ExprNode::ScalarRhs(op, Box::new(e.fuse_fma()), s),
ExprNode::ScalarLhs(op, s, e) => ExprNode::ScalarLhs(op, s, Box::new(e.fuse_fma())),
ExprNode::Unary(op, e) => ExprNode::Unary(op, Box::new(e.fuse_fma())),
ExprNode::Fma(a, b, c) => ExprNode::Fma(
Box::new(a.fuse_fma()),
Box::new(b.fuse_fma()),
Box::new(c.fuse_fma()),
),
leaf => leaf,
}
}
pub fn abs(self) -> Self {
ExprNode::Unary(UnaryOp::Abs, Box::new(self))
}
pub fn sqrt(self) -> Self {
ExprNode::Unary(UnaryOp::Sqrt, Box::new(self))
}
pub fn exp(self) -> Self {
ExprNode::Unary(UnaryOp::Exp, Box::new(self))
}
pub fn ln(self) -> Self {
ExprNode::Unary(UnaryOp::Ln, Box::new(self))
}
}
pub trait IntoExpr<T: Clone> {
fn expr(&self) -> ExprNode<T>;
}
impl<T: Clone> IntoExpr<T> for Array<T> {
fn expr(&self) -> ExprNode<T> {
ExprNode::Leaf(self.clone())
}
}
macro_rules! impl_node_binop {
($trait:ident, $method:ident, $variant:ident) => {
impl<T> $trait<ExprNode<T>> for ExprNode<T> {
type Output = ExprNode<T>;
fn $method(self, rhs: ExprNode<T>) -> ExprNode<T> {
ExprNode::Binary(BinOp::$variant, Box::new(self), Box::new(rhs))
}
}
};
}
impl_node_binop!(Add, add, Add);
impl_node_binop!(Sub, sub, Sub);
impl_node_binop!(Mul, mul, Mul);
impl_node_binop!(Div, div, Div);
impl<T> Neg for ExprNode<T> {
type Output = ExprNode<T>;
fn neg(self) -> ExprNode<T> {
ExprNode::Unary(UnaryOp::Neg, Box::new(self))
}
}
macro_rules! impl_scalar_rhs {
($ty:ty, $trait:ident, $method:ident, $variant:ident) => {
impl $trait<$ty> for ExprNode<$ty> {
type Output = ExprNode<$ty>;
fn $method(self, rhs: $ty) -> ExprNode<$ty> {
ExprNode::ScalarRhs(BinOp::$variant, Box::new(self), rhs)
}
}
};
}
macro_rules! impl_scalar_lhs {
($ty:ty, $trait:ident, $method:ident, $variant:ident) => {
impl $trait<ExprNode<$ty>> for $ty {
type Output = ExprNode<$ty>;
fn $method(self, rhs: ExprNode<$ty>) -> ExprNode<$ty> {
ExprNode::ScalarLhs(BinOp::$variant, self, Box::new(rhs))
}
}
};
}
macro_rules! impl_scalar_ops {
($ty:ty) => {
impl_scalar_rhs!($ty, Add, add, Add);
impl_scalar_rhs!($ty, Sub, sub, Sub);
impl_scalar_rhs!($ty, Mul, mul, Mul);
impl_scalar_rhs!($ty, Div, div, Div);
impl_scalar_lhs!($ty, Add, add, Add);
impl_scalar_lhs!($ty, Sub, sub, Sub);
impl_scalar_lhs!($ty, Mul, mul, Mul);
impl_scalar_lhs!($ty, Div, div, Div);
};
}
impl_scalar_ops!(f64);
impl_scalar_ops!(f32);
impl<T> ExprNode<T>
where
T: Clone
+ 'static
+ Add<Output = T>
+ Sub<Output = T>
+ Mul<Output = T>
+ Div<Output = T>
+ Neg<Output = T>,
{
pub fn eval(&self) -> Result<Array<T>> {
super::fused_eval::eval(self)
}
pub fn will_fuse(&self) -> bool {
super::fused_eval::will_fuse(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn expr_wraps_array_without_moving_it() {
let a = Array::from_vec(vec![1.0_f64, 2.0, 3.0]);
let e = a.expr();
assert!(matches!(e, ExprNode::Leaf(_)));
assert_eq!(a.to_vec(), vec![1.0, 2.0, 3.0]);
}
#[test]
fn leaf_shares_storage_with_its_source() {
let a = Array::from_vec(vec![1.0_f64, 2.0, 3.0]);
assert!(a.is_unique());
let e = a.expr();
assert!(!a.is_unique());
drop(e);
assert!(a.is_unique());
}
#[test]
fn operators_build_the_expected_tree() {
let a = Array::from_vec(vec![1.0_f64, 2.0]);
let e = a.expr() + a.expr() * a.expr();
match &e {
ExprNode::Binary(BinOp::Add, l, r) => {
assert!(matches!(**l, ExprNode::Leaf(_)));
assert!(matches!(**r, ExprNode::Binary(BinOp::Mul, _, _)));
}
other => panic!("unexpected tree: {other:?}"),
}
assert_eq!(e.leaf_count(), 3);
assert_eq!(e.depth(), 3);
}
#[test]
fn scalar_operators_build_scalar_nodes_on_both_sides() {
let a = Array::from_vec(vec![1.0_f64, 2.0]);
assert!(matches!(
a.expr() + 1.0,
ExprNode::ScalarRhs(BinOp::Add, _, _)
));
assert!(matches!(
2.0 - a.expr(),
ExprNode::ScalarLhs(BinOp::Sub, _, _)
));
let b = Array::from_vec(vec![1.0_f32, 2.0]);
assert!(matches!(
b.expr() / 4.0_f32,
ExprNode::ScalarRhs(BinOp::Div, _, _)
));
assert!(matches!(
4.0_f32 / b.expr(),
ExprNode::ScalarLhs(BinOp::Div, _, _)
));
}
#[test]
fn neg_and_math_builders() {
let a = Array::from_vec(vec![1.0_f64, 2.0]);
assert!(matches!(-a.expr(), ExprNode::Unary(UnaryOp::Neg, _)));
assert!(matches!(a.expr().abs(), ExprNode::Unary(UnaryOp::Abs, _)));
assert!(matches!(a.expr().sqrt(), ExprNode::Unary(UnaryOp::Sqrt, _)));
assert!(matches!(a.expr().exp(), ExprNode::Unary(UnaryOp::Exp, _)));
assert!(matches!(a.expr().ln(), ExprNode::Unary(UnaryOp::Ln, _)));
}
#[test]
fn fuse_fma_rewrites_canonical_order_only() {
let a = Array::from_vec(vec![1.0_f64, 2.0]);
let canonical = (a.expr() * a.expr() + a.expr()).fuse_fma();
assert!(matches!(canonical, ExprNode::Fma(..)));
let swapped = (a.expr() + a.expr() * a.expr()).fuse_fma();
assert!(matches!(swapped, ExprNode::Binary(BinOp::Add, _, _)));
}
#[test]
fn fuse_fma_rewrites_nested_occurrences() {
let a = Array::from_vec(vec![1.0_f64, 2.0]);
let e = ((a.expr() * a.expr() + a.expr()) * a.expr() + a.expr()).fuse_fma();
match e {
ExprNode::Fma(x, _, _) => assert!(matches!(*x, ExprNode::Fma(..))),
other => panic!("outer rewrite missing: {other:?}"),
}
}
#[test]
fn debug_shows_structure_not_data() {
let a = Array::from_vec(vec![1.0_f64; 1000]);
let s = format!("{:?}", a.expr() + a.expr() * 2.0);
assert_eq!(s, "(Leaf(shape=[1000]) + (Leaf(shape=[1000]) * 2.0))");
}
#[test]
fn clone_of_a_tree_copies_no_data() {
let a = Array::from_vec(vec![1.0_f64; 64]);
let e = a.expr() + a.expr();
let f = e.clone();
assert_eq!(f.leaf_count(), 2);
assert!(!a.is_unique());
}
#[test]
fn depth_and_leaf_count_cover_every_variant() {
let a = Array::from_vec(vec![1.0_f64, 2.0]);
let e = ExprNode::Fma(
Box::new(a.expr()),
Box::new(ExprNode::ScalarLhs(
BinOp::Sub,
1.0,
Box::new(a.expr().sqrt()),
)),
Box::new(ExprNode::ScalarRhs(BinOp::Div, Box::new(-a.expr()), 3.0)),
);
assert_eq!(e.leaf_count(), 3);
assert_eq!(e.depth(), 4);
}
}