use crate::array::Array;
use crate::error::{NumRs2Error, Result};
use crate::kernels::{borrow, cast};
use std::any::TypeId;
use std::ops::{Add, Div, Mul, Neg, Sub};
use super::owned::{BinOp, ExprNode, UnaryOp};
trait FusedElem:
Copy
+ 'static
+ Add<Output = Self>
+ Sub<Output = Self>
+ Mul<Output = Self>
+ Div<Output = Self>
+ Neg<Output = Self>
{
fn from_slice<T: 'static>(s: &[T]) -> Option<&[Self]>;
fn from_scalar<T: 'static>(x: &T) -> Option<Self>;
fn into_vec<T: 'static>(v: Vec<Self>) -> Option<Vec<T>>;
fn abs(self) -> Self;
fn sqrt(self) -> Self;
fn exp(self) -> Self;
fn ln(self) -> Self;
}
impl FusedElem for f64 {
fn from_slice<T: 'static>(s: &[T]) -> Option<&[f64]> {
cast::as_f64(s)
}
fn from_scalar<T: 'static>(x: &T) -> Option<f64> {
cast::as_f64(std::slice::from_ref(x)).and_then(|s| s.first().copied())
}
fn into_vec<T: 'static>(v: Vec<f64>) -> Option<Vec<T>> {
cast::vec_from_f64(v)
}
fn abs(self) -> f64 {
f64::abs(self)
}
fn sqrt(self) -> f64 {
f64::sqrt(self)
}
fn exp(self) -> f64 {
f64::exp(self)
}
fn ln(self) -> f64 {
f64::ln(self)
}
}
impl FusedElem for f32 {
fn from_slice<T: 'static>(s: &[T]) -> Option<&[f32]> {
cast::as_f32(s)
}
fn from_scalar<T: 'static>(x: &T) -> Option<f32> {
cast::as_f32(std::slice::from_ref(x)).and_then(|s| s.first().copied())
}
fn into_vec<T: 'static>(v: Vec<f32>) -> Option<Vec<T>> {
cast::vec_from_f32(v)
}
fn abs(self) -> f32 {
f32::abs(self)
}
fn sqrt(self) -> f32 {
f32::sqrt(self)
}
fn exp(self) -> f32 {
f32::exp(self)
}
fn ln(self) -> f32 {
f32::ln(self)
}
}
fn plan<T: Clone + 'static>(root: &ExprNode<T>) -> Option<(Vec<usize>, usize)> {
if TypeId::of::<T>() != TypeId::of::<f64>() && TypeId::of::<T>() != TypeId::of::<f32>() {
return None;
}
let mut leaves: Vec<&Array<T>> = Vec::new();
root.collect_leaves(&mut leaves);
let first = leaves.first()?;
let shape = first.shape();
for leaf in &leaves {
if leaf.shape() != shape || leaf.as_slice().is_none() {
return None;
}
}
let n = first.size();
Some((shape, n))
}
pub(super) fn will_fuse<T: Clone + 'static>(root: &ExprNode<T>) -> bool {
plan(root).is_some() && is_specialized_shape(root)
}
macro_rules! with_binop {
($op:expr, $f:ident, $body:expr) => {
match $op {
BinOp::Add => {
let $f = |x: S, y: S| x + y;
$body
}
BinOp::Sub => {
let $f = |x: S, y: S| x - y;
$body
}
BinOp::Mul => {
let $f = |x: S, y: S| x * y;
$body
}
BinOp::Div => {
let $f = |x: S, y: S| x / y;
$body
}
}
};
}
fn map1<S: Copy, F: Fn(S) -> S>(x: &[S], f: F) -> Vec<S> {
x.iter().map(|&a| f(a)).collect()
}
fn zip2<S: Copy, F: Fn(S, S) -> S>(x: &[S], y: &[S], f: F) -> Vec<S> {
x.iter().zip(y.iter()).map(|(&a, &b)| f(a, b)).collect()
}
fn zip3_left<S: Copy, FI: Fn(S, S) -> S, FO: Fn(S, S) -> S>(
x: &[S],
y: &[S],
z: &[S],
inner: FI,
outer: FO,
) -> Vec<S> {
x.iter()
.zip(y.iter())
.zip(z.iter())
.map(|((&a, &b), &c)| outer(inner(a, b), c))
.collect()
}
fn zip3_right<S: Copy, FI: Fn(S, S) -> S, FO: Fn(S, S) -> S>(
x: &[S],
y: &[S],
z: &[S],
inner: FI,
outer: FO,
) -> Vec<S> {
x.iter()
.zip(y.iter())
.zip(z.iter())
.map(|((&a, &b), &c)| outer(a, inner(b, c)))
.collect()
}
fn zip4<S: Copy, FL: Fn(S, S) -> S, FR: Fn(S, S) -> S, FO: Fn(S, S) -> S>(
w: &[S],
x: &[S],
y: &[S],
z: &[S],
left: FL,
right: FR,
outer: FO,
) -> Vec<S> {
w.iter()
.zip(x.iter())
.zip(y.iter())
.zip(z.iter())
.map(|(((&a, &b), &c), &d)| outer(left(a, b), right(c, d)))
.collect()
}
fn apply_unary<S: FusedElem>(op: UnaryOp, x: &[S]) -> Vec<S> {
match op {
UnaryOp::Neg => map1(x, |a| -a),
UnaryOp::Abs => map1(x, S::abs),
UnaryOp::Sqrt => map1(x, S::sqrt),
UnaryOp::Exp => map1(x, S::exp),
UnaryOp::Ln => map1(x, S::ln),
}
}
fn leaf_slice<T: Clone + 'static, S: FusedElem>(node: &ExprNode<T>) -> Option<&[S]> {
match node {
ExprNode::Leaf(a) => S::from_slice(a.as_slice()?),
_ => None,
}
}
fn fused_specialized<T: Clone + 'static, S: FusedElem>(root: &ExprNode<T>) -> Option<Vec<S>> {
match root {
ExprNode::Leaf(_) => leaf_slice::<T, S>(root).map(<[S]>::to_vec),
ExprNode::Binary(op, l, r) => {
if let (Some(x), Some(y)) = (leaf_slice::<T, S>(l), leaf_slice::<T, S>(r)) {
return Some(with_binop!(*op, f, zip2(x, y, f)));
}
if let (ExprNode::Binary(iop, ll, lr), Some(z)) = (&**l, leaf_slice::<T, S>(r)) {
if let (Some(x), Some(y)) = (leaf_slice::<T, S>(ll), leaf_slice::<T, S>(lr)) {
return Some(with_binop!(
*iop,
fi,
with_binop!(*op, fo, zip3_left(x, y, z, fi, fo))
));
}
}
if let (Some(x), ExprNode::Binary(iop, rl, rr)) = (leaf_slice::<T, S>(l), &**r) {
if let (Some(y), Some(z)) = (leaf_slice::<T, S>(rl), leaf_slice::<T, S>(rr)) {
return Some(with_binop!(
*iop,
fi,
with_binop!(*op, fo, zip3_right(x, y, z, fi, fo))
));
}
}
if let (ExprNode::ScalarRhs(sop, se, k), Some(y)) = (&**l, leaf_slice::<T, S>(r)) {
if let (Some(x), Some(k)) = (leaf_slice::<T, S>(se), S::from_scalar(k)) {
return Some(with_binop!(
*sop,
fs,
with_binop!(*op, fo, zip2(x, y, |a, b| fo(fs(a, k), b)))
));
}
}
if let (Some(x), ExprNode::ScalarRhs(sop, se, k)) = (leaf_slice::<T, S>(l), &**r) {
if let (Some(y), Some(k)) = (leaf_slice::<T, S>(se), S::from_scalar(k)) {
return Some(with_binop!(
*sop,
fs,
with_binop!(*op, fo, zip2(x, y, |a, b| fo(a, fs(b, k))))
));
}
}
if let (ExprNode::Binary(lop, ll, lr), ExprNode::Binary(rop, rl, rr)) = (&**l, &**r) {
if let (Some(w), Some(x), Some(y), Some(z)) = (
leaf_slice::<T, S>(ll),
leaf_slice::<T, S>(lr),
leaf_slice::<T, S>(rl),
leaf_slice::<T, S>(rr),
) {
return Some(with_binop!(
*lop,
fl,
with_binop!(*rop, fr, with_binop!(*op, fo, zip4(w, x, y, z, fl, fr, fo)))
));
}
}
None
}
ExprNode::Fma(a, b, c) => {
let (x, y, z) = (
leaf_slice::<T, S>(a)?,
leaf_slice::<T, S>(b)?,
leaf_slice::<T, S>(c)?,
);
Some(zip3_left(x, y, z, |p, q| p * q, |p, q| p + q))
}
ExprNode::ScalarRhs(op, e, s) => {
let x = leaf_slice::<T, S>(e)?;
let k = S::from_scalar(s)?;
Some(with_binop!(*op, f, map1(x, |a| f(a, k))))
}
ExprNode::ScalarLhs(op, s, e) => {
let x = leaf_slice::<T, S>(e)?;
let k = S::from_scalar(s)?;
Some(with_binop!(*op, f, map1(x, |a| f(k, a))))
}
ExprNode::Unary(op, e) => {
let x = leaf_slice::<T, S>(e)?;
Some(apply_unary(*op, x))
}
}
}
fn is_leaf<T>(node: &ExprNode<T>) -> bool {
matches!(node, ExprNode::Leaf(_))
}
fn is_specialized_shape<T>(root: &ExprNode<T>) -> bool {
match root {
ExprNode::Leaf(_) => true,
ExprNode::Binary(_, l, r) => {
(is_leaf(l) && is_leaf(r))
|| (matches!(&**l, ExprNode::Binary(_, ll, lr) if is_leaf(ll) && is_leaf(lr))
&& is_leaf(r))
|| (is_leaf(l)
&& matches!(&**r, ExprNode::Binary(_, rl, rr) if is_leaf(rl) && is_leaf(rr)))
|| (matches!(&**l, ExprNode::ScalarRhs(_, se, _) if is_leaf(se)) && is_leaf(r))
|| (is_leaf(l)
&& matches!(&**r, ExprNode::ScalarRhs(_, se, _) if is_leaf(se)))
|| matches!(
(&**l, &**r),
(ExprNode::Binary(_, ll, lr), ExprNode::Binary(_, rl, rr))
if is_leaf(ll) && is_leaf(lr) && is_leaf(rl) && is_leaf(rr)
)
}
ExprNode::Fma(a, b, c) => is_leaf(a) && is_leaf(b) && is_leaf(c),
ExprNode::ScalarRhs(_, e, _) | ExprNode::ScalarLhs(_, _, e) => is_leaf(e),
ExprNode::Unary(_, e) => is_leaf(e),
}
}
fn unary_math_eager<T: Clone + 'static>(a: &Array<T>, op: UnaryOp) -> Result<Array<T>> {
let shape = a.shape();
let src = borrow::operand(a);
if let Some(s) = cast::as_f64(&src) {
let out = apply_unary(op, s);
if let Some(data) = cast::vec_from_f64::<T>(out) {
return Array::from_vec_shape(data, &shape);
}
}
if let Some(s) = cast::as_f32(&src) {
let out = apply_unary(op, s);
if let Some(data) = cast::vec_from_f32::<T>(out) {
return Array::from_vec_shape(data, &shape);
}
}
Err(NumRs2Error::NotImplemented(format!(
"expression unary op `{op:?}` is implemented for f64 and f32 only"
)))
}
fn eval_eager<T>(node: &ExprNode<T>) -> Result<Array<T>>
where
T: Clone
+ 'static
+ Add<Output = T>
+ Sub<Output = T>
+ Mul<Output = T>
+ Div<Output = T>
+ Neg<Output = T>,
{
match node {
ExprNode::Leaf(a) => Ok(a.clone()),
ExprNode::Binary(op, l, r) => {
let lv = eval_eager(l)?;
let rv = eval_eager(r)?;
match op {
BinOp::Add => lv.add_broadcast(&rv),
BinOp::Sub => lv.subtract_broadcast(&rv),
BinOp::Mul => lv.multiply_broadcast(&rv),
BinOp::Div => lv.divide_broadcast(&rv),
}
}
ExprNode::ScalarRhs(op, e, s) => {
let v = eval_eager(e)?;
Ok(match op {
BinOp::Add => v.add_scalar(s.clone()),
BinOp::Sub => v.subtract_scalar(s.clone()),
BinOp::Mul => v.multiply_scalar(s.clone()),
BinOp::Div => v.divide_scalar(s.clone()),
})
}
ExprNode::ScalarLhs(op, s, e) => {
let v = eval_eager(e)?;
Ok(match op {
BinOp::Add => v.map(|x| s.clone() + x),
BinOp::Sub => v.map(|x| s.clone() - x),
BinOp::Mul => v.map(|x| s.clone() * x),
BinOp::Div => v.map(|x| s.clone() / x),
})
}
ExprNode::Unary(UnaryOp::Neg, e) => Ok(eval_eager(e)?.map(|x| -x)),
ExprNode::Unary(op, e) => unary_math_eager(&eval_eager(e)?, *op),
ExprNode::Fma(a, b, c) => {
let av = eval_eager(a)?;
let bv = eval_eager(b)?;
let prod = av.multiply_broadcast(&bv)?;
let cv = eval_eager(c)?;
prod.add_broadcast(&cv)
}
}
}
pub(super) fn eval<T>(root: &ExprNode<T>) -> Result<Array<T>>
where
T: Clone
+ 'static
+ Add<Output = T>
+ Sub<Output = T>
+ Mul<Output = T>
+ Div<Output = T>
+ Neg<Output = T>,
{
let Some((shape, _n)) = plan(root) else {
return eval_eager(root);
};
if TypeId::of::<T>() == TypeId::of::<f64>() {
if let Some(out) = fused_specialized::<T, f64>(root) {
if let Some(data) = <f64 as FusedElem>::into_vec::<T>(out) {
return Array::from_vec_shape(data, &shape);
}
}
} else if let Some(out) = fused_specialized::<T, f32>(root) {
if let Some(data) = <f32 as FusedElem>::into_vec::<T>(out) {
return Array::from_vec_shape(data, &shape);
}
}
eval_eager(root)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::expr::owned::IntoExpr;
fn seq(n: usize, scale: f64, offset: f64) -> Array<f64> {
Array::from_vec((0..n).map(|i| i as f64 * scale + offset).collect())
}
fn assert_bit_eq(got: &Array<f64>, want: &Array<f64>, what: &str) {
assert_eq!(got.shape(), want.shape(), "{what}: shape");
for (i, (g, w)) in got.to_vec().iter().zip(want.to_vec().iter()).enumerate() {
assert_eq!(
g.to_bits(),
w.to_bits(),
"{what}: element {i}: {g} ({:#018x}) vs {w} ({:#018x})",
g.to_bits(),
w.to_bits()
);
}
}
#[test]
fn fuses_the_canonical_chain_and_matches_eager() -> Result<()> {
for n in [0usize, 1, 7, 1_023, 1_024, 1_025, 5_000] {
let a = seq(n, 1.0, 0.5);
let b = seq(n, -0.25, 3.0);
let c = seq(n, 0.125, -2.0);
let e = a.expr() + b.expr() * c.expr();
assert!(e.will_fuse(), "n={n}");
let fused = e.eval()?;
let eager = &a + &(&b * &c);
assert_bit_eq(&fused, &eager, &format!("a + b*c, n={n}"));
}
Ok(())
}
#[test]
fn fma_node_is_two_roundings_not_mul_add() {
let x = 1.0_f64 + 2.0_f64.powi(-27);
let a = Array::from_vec(vec![x]);
let b = Array::from_vec(vec![x]);
let c = Array::from_vec(vec![-1.0_f64]);
let node = (a.expr() * b.expr() + c.expr()).fuse_fma();
assert!(matches!(node, ExprNode::Fma(..)));
let got = node.eval().expect("fma eval");
let two_roundings = x * x - 1.0;
let one_rounding = x.mul_add(x, -1.0);
assert_ne!(
two_roundings.to_bits(),
one_rounding.to_bits(),
"test is vacuous unless these differ"
);
assert_eq!(got.to_vec()[0].to_bits(), two_roundings.to_bits());
}
#[test]
fn unspecialised_shapes_fall_back_to_eager_and_match() -> Result<()> {
let n = 3_000;
let a = seq(n, 1.0, 0.5);
let b = seq(n, -0.25, 3.0);
let c = seq(n, 0.125, -2.0);
let d = seq(n, 2.0, 1.0);
let e = seq(n, -1.5, 0.25);
let tree = ((a.expr() + b.expr()) * (c.expr() - d.expr())) / e.expr();
assert!(!tree.will_fuse(), "5 leaves is past the specialised set");
assert!(fused_specialized::<f64, f64>(&tree).is_none());
let fused = tree.eval()?;
let eager = &(&(&a + &b) * &(&c - &d)) / &e;
assert_bit_eq(&fused, &eager, "((a+b)*(c-d))/e");
Ok(())
}
#[test]
fn four_leaf_shape_is_specialised_and_matches_eager() -> Result<()> {
let n = 3_000;
let a = seq(n, 1.0, 0.5);
let b = seq(n, -0.25, 3.0);
let c = seq(n, 0.125, -2.0);
let d = seq(n, 2.0, 1.0);
let tree = (a.expr() + b.expr()) * (c.expr() - d.expr());
assert!(tree.will_fuse());
assert!(
fused_specialized::<f64, f64>(&tree).is_some(),
"(a+b)*(c-d) must hit the zip4 loop, not the eager fallback"
);
assert_bit_eq(&tree.eval()?, &(&(&a + &b) * &(&c - &d)), "(a+b)*(c-d)");
let tree2 = (a.expr() / b.expr()) - (c.expr() * d.expr());
assert!(fused_specialized::<f64, f64>(&tree2).is_some());
assert_bit_eq(&tree2.eval()?, &(&(&a / &b) - &(&c * &d)), "(a/b)-(c*d)");
Ok(())
}
#[test]
fn every_specialised_shape_matches_eager() -> Result<()> {
let n = 2_000;
let a = seq(n, 1.0, 1.5);
let b = seq(n, 0.5, 2.5);
let c = seq(n, 0.25, 0.75);
assert_bit_eq(&(a.expr() - b.expr()).eval()?, &(&a - &b), "leaf-leaf");
assert_bit_eq(
&((a.expr() / b.expr()) * c.expr()).eval()?,
&(&(&a / &b) * &c),
"chain-left",
);
assert_bit_eq(
&(a.expr() - b.expr() / c.expr()).eval()?,
&(&a - &(&b / &c)),
"chain-right",
);
assert_bit_eq(&(a.expr() * 3.0).eval()?, &(&a * 3.0), "scalar-rhs");
assert_bit_eq(&(3.0 - a.expr()).eval()?, &a.map(|x| 3.0 - x), "scalar-lhs");
assert_bit_eq(&(-a.expr()).eval()?, &(-&a), "neg");
assert_bit_eq(&a.expr().sqrt().eval()?, &a.map(f64::sqrt), "sqrt");
assert_bit_eq(&a.expr().eval()?, &a, "bare leaf");
Ok(())
}
#[test]
fn non_contiguous_leaf_falls_back_and_matches() -> Result<()> {
let a = Array::from_vec((0..12).map(|i| i as f64).collect()).reshape(&[3, 4]);
let t = a.transpose_axis(0, 1);
assert!(!t.is_c_contiguous());
let e = t.expr() * t.expr() + t.expr();
assert!(!e.will_fuse(), "non-contiguous leaf must not fuse");
let got = e.eval()?;
let want = &(&t * &t) + &t;
assert_bit_eq(&got, &want, "transposed leaves");
Ok(())
}
#[test]
fn broadcast_shapes_fall_back_and_match() -> Result<()> {
let row = Array::from_vec(vec![1.0_f64, 2.0, 3.0]).reshape(&[1, 3]);
let col = Array::from_vec(vec![10.0_f64, 20.0, 30.0]).reshape(&[3, 1]);
let e = row.expr() + col.expr();
assert!(!e.will_fuse());
let got = e.eval()?;
assert_eq!(got.shape(), vec![3, 3]);
assert_bit_eq(&got, &(&row + &col), "broadcast");
Ok(())
}
#[test]
fn incompatible_shapes_error_like_the_eager_op() {
let a = Array::from_vec(vec![1.0_f64, 2.0, 3.0]);
let b = Array::from_vec(vec![1.0_f64, 2.0]);
let e = a.expr() + b.expr();
assert!(!e.will_fuse());
assert!(e.eval().is_err());
assert!(a.add_broadcast(&b).is_err());
}
#[test]
fn f32_fuses_too() -> Result<()> {
let n = 1_500;
let a = Array::from_vec((0..n).map(|i| i as f32 * 0.5).collect());
let b = Array::from_vec((0..n).map(|i| i as f32 - 3.0).collect());
let e = a.expr() * b.expr() + b.expr();
assert!(e.will_fuse());
let got = e.eval()?;
let want = &(&a * &b) + &b;
for (g, w) in got.to_vec().iter().zip(want.to_vec().iter()) {
assert_eq!(g.to_bits(), w.to_bits());
}
Ok(())
}
#[test]
fn integer_dtype_falls_back_and_matches() -> Result<()> {
let a = Array::from_vec(vec![1_i64, 2, 3, 4]);
let b = Array::from_vec(vec![10_i64, 20, 30, 40]);
let e = a.expr() * b.expr() + 5_i64.into_expr_node();
assert!(!e.will_fuse(), "i64 has no fused path");
assert_eq!(e.eval()?.to_vec(), vec![15, 45, 95, 165]);
Ok(())
}
trait IntoExprNode {
fn into_expr_node(self) -> ExprNode<i64>;
}
impl IntoExprNode for i64 {
fn into_expr_node(self) -> ExprNode<i64> {
ExprNode::Leaf(Array::from_vec(vec![self; 4]))
}
}
#[test]
fn maths_op_on_integer_dtype_is_a_clean_error() {
let a = Array::from_vec(vec![4_i64, 9]);
let e = a.expr().sqrt();
let err = e.eval().expect_err("sqrt on i64 must not silently succeed");
assert!(matches!(err, NumRs2Error::NotImplemented(_)), "{err:?}");
}
#[test]
fn special_values_match_eager() -> Result<()> {
let a = Array::from_vec(vec![
0.0_f64,
-0.0,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NAN,
1.0,
]);
let b = Array::from_vec(vec![-0.0_f64, 0.0, 1.0, f64::INFINITY, 2.0, f64::NAN]);
let c = Array::from_vec(vec![1.0_f64, -1.0, 0.0, -0.0, f64::NAN, 3.0]);
let e = a.expr() + b.expr() * c.expr();
assert!(e.will_fuse());
assert_bit_eq(&e.eval()?, &(&a + &(&b * &c)), "special values");
Ok(())
}
#[test]
fn signed_zeros_are_preserved_exactly() -> Result<()> {
let a = Array::from_vec(vec![-0.0_f64, -0.0, 0.0, 0.0]);
let b = Array::from_vec(vec![-0.0_f64, 0.0, -0.0, 0.0]);
let c = Array::from_vec(vec![1.0_f64; 4]);
let got = (a.expr() + b.expr() * c.expr()).eval()?;
let want = &a + &(&b * &c);
for (i, (g, w)) in got.to_vec().iter().zip(want.to_vec().iter()).enumerate() {
assert_eq!(
g.to_bits(),
w.to_bits(),
"element {i}: {g} vs {w} -- signed zeros must survive fusion"
);
}
assert_eq!(got.to_vec()[0].to_bits(), (-0.0_f64).to_bits());
assert_eq!(got.to_vec()[3].to_bits(), 0.0_f64.to_bits());
Ok(())
}
#[test]
fn recognizer_agrees_with_the_specialised_arms() {
let a = Array::from_vec(vec![1.0_f64, 2.0, 3.0, 4.0]);
let l = || a.expr();
let trees: Vec<ExprNode<f64>> = vec![
l(),
l() + l(),
(l() + l()) * l(),
l() - (l() / l()),
(l() + l()) * (l() - l()),
ExprNode::Fma(Box::new(l()), Box::new(l()), Box::new(l())),
l() * 2.0,
2.0 - l(),
-l(),
l().sqrt(),
((l() + l()) * (l() - l())) / l(),
l() * 2.0 + l(),
ExprNode::Fma(Box::new(l() + l()), Box::new(l()), Box::new(l())),
(l() + l()) + ((l() + l()) + l()),
(-l()).abs(),
];
for t in &trees {
assert_eq!(
is_specialized_shape(t),
fused_specialized::<f64, f64>(t).is_some(),
"recognizer disagrees with the evaluator for {t:?}"
);
}
}
#[test]
fn deep_tree_agrees_with_eager() -> Result<()> {
let n = 2_500;
let a = seq(n, 1.0, 1.0);
let b = seq(n, 0.5, 2.0);
let c = seq(n, 0.25, 3.0);
let d = seq(n, 0.125, 4.0);
let e = ((a.expr() * b.expr() + c.expr()) / (d.expr() - 1.0)).abs() + (2.0 * a.expr());
assert!(
!e.will_fuse(),
"depth-5 mixed tree is past the specialised set"
);
let got = e.eval()?;
let quotient = &(&(&a * &b) + &c) / &(&d - 1.0);
let want = "ient.map(f64::abs) + &a.map(|x| 2.0 * x);
assert_bit_eq(&got, &want, "depth-5 mixed tree");
Ok(())
}
#[test]
fn empty_arrays_evaluate_to_empty() -> Result<()> {
let a: Array<f64> = Array::from_vec(vec![]);
let e = a.expr() + a.expr() * a.expr();
assert!(e.will_fuse());
let got = e.eval()?;
assert_eq!(got.size(), 0);
Ok(())
}
#[test]
fn multi_dimensional_shape_is_preserved() -> Result<()> {
let a = Array::from_vec((0..24).map(|i| i as f64).collect()).reshape(&[2, 3, 4]);
let e = a.expr() * 2.0 + a.expr();
assert!(e.will_fuse(), "axpy shape must fuse");
let got = e.eval()?;
assert_eq!(got.shape(), vec![2, 3, 4]);
assert_bit_eq(&got, &(&(&a * 2.0) + &a), "3-D shape");
Ok(())
}
}