use super::*;
use crate::types::ExpressionNode;
use ndarray::{ArrayViewD, Slice};
thread_local! {
static OVERLAY_OFF: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
pub(super) fn overlay_off() -> bool {
OVERLAY_OFF.with(std::cell::Cell::get)
}
pub(super) struct OverlayGuard(bool);
impl OverlayGuard {
pub(super) fn armed(off: bool) -> Self {
OverlayGuard(OVERLAY_OFF.with(|c| c.replace(off)))
}
}
impl Drop for OverlayGuard {
fn drop(&mut self) {
OVERLAY_OFF.with(|c| c.set(self.0));
}
}
thread_local! {
static OVERLAY_DECLINE: std::cell::Cell<Option<&'static str>> =
const { std::cell::Cell::new(None) };
}
pub(super) fn note_overlay_decline(site: &'static str) {
OVERLAY_DECLINE.with(|c| {
if c.get().is_none() {
c.set(Some(site));
}
});
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn take_overlay_decline() -> Option<&'static str> {
OVERLAY_DECLINE.with(|c| c.take())
}
macro_rules! bail_vec {
($site:literal) => {{
$crate::simulate_array::vectorized::note_overlay_decline($site);
return None;
}};
($site:literal, $fmt:expr) => {{
$crate::simulate_array::vectorized::note_overlay_decline($site);
let _ = &$fmt;
return None;
}};
}
pub(super) enum VecValue<'a> {
Scalar(f64),
View {
data: ArrayViewD<'a, f64>,
origin: DimI,
},
Owned {
data: ArrayD<f64>,
origin: DimI,
},
}
impl<'a> VecValue<'a> {
pub(super) fn origin(&self) -> Option<&[i64]> {
match self {
VecValue::Scalar(_) => None,
VecValue::View { origin, .. } | VecValue::Owned { origin, .. } => Some(origin),
}
}
pub(super) fn shape(&self) -> Option<&[usize]> {
match self {
VecValue::Scalar(_) => None,
VecValue::View { data, .. } => Some(data.shape()),
VecValue::Owned { data, .. } => Some(data.shape()),
}
}
pub(super) fn view(&self) -> Option<ArrayViewD<'_, f64>> {
match self {
VecValue::Scalar(_) => None,
VecValue::View { data, .. } => Some(data.view()),
VecValue::Owned { data, .. } => Some(data.view()),
}
}
fn into_owned(self, pool: &mut Pool) -> (ArrayD<f64>, DimI) {
match self {
VecValue::Owned { data, origin } => (data, origin),
VecValue::View { data, origin } => {
let mut buf = pool.take_array_uninit(data.shape());
buf.assign(&data);
(buf, origin)
}
VecValue::Scalar(_) => unreachable!("into_owned called on a scalar"),
}
}
pub(super) fn release(self, pool: &mut Pool) {
if let VecValue::Owned { data, .. } = self {
pool.give_array(data);
}
}
}
pub(super) struct VecBox<'a> {
pub(super) syms: &'a [String],
pub(super) lo: &'a [i64],
pub(super) shape: &'a [usize],
pub(super) cnames: &'a [String],
pub(super) cvals: &'a [i64],
}
impl<'a> VecBox<'a> {
fn cbind(&self, name: &str) -> Option<i64> {
self.cnames
.iter()
.position(|n| n == name)
.map(|i| self.cvals[i])
}
}
pub(super) fn lhs_constant_shifts(
lhs_idx_exprs: &[Expr],
output_idx_names: &[String],
) -> Option<SmallVec<[i64; 4]>> {
if lhs_idx_exprs.len() != output_idx_names.len() {
return None;
}
let nobind = VecBox {
syms: &[],
lo: &[],
shape: &[],
cnames: &[],
cvals: &[],
};
let mut shifts = SmallVec::new();
for (e, sym) in lhs_idx_exprs.iter().zip(output_idx_names.iter()) {
shifts.push(affine_offset_in(e, sym, &nobind)?);
}
Some(shifts)
}
pub(super) fn subblock_dest(
vs: &VarShape,
output_ranges: &[(i64, i64)],
shifts: &[i64],
) -> Option<SmallVec<[usize; 4]>> {
if vs.shape.len() != output_ranges.len() || shifts.len() != output_ranges.len() {
return None;
}
let mut dest = SmallVec::new();
for d in 0..output_ranges.len() {
let (olo, ohi) = output_ranges[d];
let extent = ohi - olo + 1;
if extent <= 0 {
return None;
}
let dlo = olo + shifts[d] - vs.origin[d];
if dlo < 0 || dlo + extent > vs.shape[d] as i64 {
return None;
}
dest.push(dlo as usize);
}
Some(dest)
}
#[allow(clippy::too_many_arguments)]
fn apply_filter_gate<'a>(
term: VecValue<'a>,
filter: Option<&Expr>,
identity: f64,
bx: &VecBox,
lo: &[i64],
shape: &[usize],
ctx: &EvalCtx<'a>,
pool: &mut Pool,
ops: &mut usize,
) -> Option<VecValue<'a>> {
let f = match filter {
None => return Some(term),
Some(f) => f,
};
match eval_vec(f, bx, ctx, pool, ops) {
None => {
term.release(pool);
None
}
Some(VecValue::Scalar(c)) => {
if c != 0.0 {
Some(term)
} else {
term.release(pool);
let mut ident = pool.take_array(shape);
if identity != 0.0 {
ident.fill(identity);
}
Some(VecValue::Owned {
data: ident,
origin: lo.iter().copied().collect(),
})
}
}
Some(mask) => vec_select(mask, term, VecValue::Scalar(identity), pool),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn try_eval_faq_vectorized<'a>(
output_idx_names: &[String],
output_ranges: &[(i64, i64)],
body: &Expr,
contract_names: &[String],
contract_dims: &[ContractDim],
reduce: ReduceKind,
filter: Option<&Expr>,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
) -> Option<(VecValue<'a>, usize)> {
if overlay_off() {
return None;
}
let lo: DimI = output_ranges.iter().map(|(l, _)| *l).collect();
let shape: DimU = output_ranges
.iter()
.map(|(l, h)| (h - l + 1) as usize)
.collect();
if shape.contains(&0) {
bail_vec!("faq: empty output box");
}
if folds_from_the_top(output_idx_names, contract_names, filter) {
bail_vec!("faq: reverse window (folds from the top)");
}
if let Some(rt) = ctx.cse {
rt.analyse(body, output_idx_names, contract_names);
}
let _cse_scope = ctx.cse.map(|rt| rt.scope(pool));
let mut ops = 0usize;
let v = if contract_names.is_empty() {
let bx = VecBox {
syms: output_idx_names,
lo: &lo[..],
shape: &shape[..],
cnames: &[],
cvals: &[],
};
let body_v = eval_vec(body, &bx, ctx, pool, &mut ops)?;
apply_filter_gate(
body_v,
filter,
reduce.identity(),
&bx,
&lo,
&shape,
ctx,
pool,
&mut ops,
)?
} else {
eval_vec_contracted(
output_idx_names,
&lo,
&shape,
body,
contract_names,
contract_dims,
reduce,
filter,
ctx,
pool,
&mut ops,
)?
};
if matches!(v, VecValue::View { .. }) {
v.release(pool);
bail_vec!("faq: body reduced to a bare whole-array view (oracle scalarizes it)");
}
let matches_box = match v.shape() {
None => true,
Some(s) => s == &shape[..] && v.origin().map(|o| o == &lo[..]).unwrap_or(false),
};
if !matches_box {
v.release(pool);
bail_vec!("faq: result box does not match the output box");
}
let out = match v {
VecValue::Scalar(s) => {
let mut buf = pool.take_array_uninit(&shape);
buf.fill(s);
VecValue::Owned {
data: buf,
origin: lo,
}
}
other => other,
};
Some((out, ops))
}
pub(super) fn reduce_combine_op(reduce: ReduceKind) -> Option<BinCode> {
match reduce {
ReduceKind::Sum => Some(BinCode::Add),
ReduceKind::Product => Some(BinCode::Mul),
ReduceKind::Max => Some(BinCode::Max),
ReduceKind::Min => Some(BinCode::Min),
ReduceKind::Or | ReduceKind::And => None,
}
}
pub(super) fn scalar_combine_op(reduce: ReduceKind) -> BinCode {
match reduce {
ReduceKind::Or => BinCode::Or,
ReduceKind::And => BinCode::And,
other => reduce_combine_op(other).expect("a numeric ⊕ has a kernel"),
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn eval_vec_contracted<'a>(
output_idx_names: &[String],
lo: &[i64],
shape: &[usize],
body: &Expr,
contract_names: &[String],
contract_dims: &[ContractDim],
reduce: ReduceKind,
filter: Option<&Expr>,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
ops: &mut usize,
) -> Option<VecValue<'a>> {
let combine_op = match reduce_combine_op(reduce) {
Some(op) => op,
None => bail_vec!("contracted: boolean reduction (or/and) not vectorized"),
};
const MAXC: usize = 4;
let nc = contract_names.len();
if nc == 0 || nc > MAXC {
bail_vec!("contracted: contraction rank out of range");
}
let mut clo = [0i64; MAXC];
let mut chi = [0i64; MAXC];
for (i, d) in contract_dims.iter().enumerate() {
match d {
ContractDim::Static(l, h) => {
clo[i] = *l;
chi[i] = *h;
}
_ => {
bail_vec!("contracted: non-static contraction dim (ragged/derived)")
}
}
}
let mut acc_buf = pool.take_array(shape);
let identity = reduce.identity();
if identity != 0.0 {
acc_buf.fill(identity);
}
let mut acc = VecValue::Owned {
data: acc_buf,
origin: lo.iter().copied().collect(),
};
if (0..nc).any(|i| clo[i] > chi[i]) {
return Some(acc);
}
let fuse_mul: Option<(&Expr, &Expr)> =
if filter.is_none() && reduce == ReduceKind::Sum && !crate::precision::is_f32() {
as_op(body, "*", 2).map(|n| (&n.args[0], &n.args[1]))
} else {
None
};
let mut cvals = [0i64; MAXC];
cvals[..nc].copy_from_slice(&clo[..nc]);
loop {
let _cse_scope = ctx.cse.map(|rt| rt.scope(pool));
let bx = VecBox {
syms: output_idx_names,
lo,
shape,
cnames: contract_names,
cvals: &cvals[..nc],
};
if let Some((ea, eb)) = fuse_mul {
let va = match eval_vec(ea, &bx, ctx, pool, ops) {
Some(v) => v,
None => {
acc.release(pool);
return None;
}
};
let vb = match eval_vec(eb, &bx, ctx, pool, ops) {
Some(v) => v,
None => {
va.release(pool);
acc.release(pool);
return None;
}
};
*ops += 1; let covers = |v: &VecValue| v.origin() == Some(lo) && v.shape() == Some(shape);
let acc_data = match &mut acc {
VecValue::Owned { data, .. } => data,
_ => unreachable!("the contraction accumulator is always Owned"),
};
match (va, vb) {
(VecValue::Scalar(x), VecValue::Scalar(y)) => {
let s = x * y;
acc_data.mapv_inplace(|a| a + s);
}
(arr, VecValue::Scalar(s)) if covers(&arr) => {
let av = arr.view().expect("array factor has a view");
ndarray::Zip::from(&mut *acc_data)
.and(&av)
.for_each(|o, &x| *o += x * s);
drop(av);
arr.release(pool);
}
(VecValue::Scalar(s), arr) if covers(&arr) => {
let av = arr.view().expect("array factor has a view");
ndarray::Zip::from(&mut *acc_data)
.and(&av)
.for_each(|o, &y| *o += s * y);
drop(av);
arr.release(pool);
}
(va, vb) => {
let term = match vec_combine(BinCode::Mul, va, vb, pool) {
Some(t) => t,
None => {
acc.release(pool);
return None;
}
};
acc = vec_combine(combine_op, acc, term, pool)?;
}
}
if !next_contraction_tuple(&mut cvals[..nc], &clo[..nc], &chi[..nc]) {
break;
}
continue;
}
let term = match eval_vec(body, &bx, ctx, pool, ops) {
Some(t) => t,
None => {
acc.release(pool);
return None;
}
};
let term = match apply_filter_gate(term, filter, identity, &bx, lo, shape, ctx, pool, ops) {
Some(t) => t,
None => {
acc.release(pool);
return None;
}
};
acc = vec_combine(combine_op, acc, term, pool)?;
if !next_contraction_tuple(&mut cvals[..nc], &clo[..nc], &chi[..nc]) {
break;
}
}
Some(acc)
}
fn next_contraction_tuple(cvals: &mut [i64], clo: &[i64], chi: &[i64]) -> bool {
let mut d = cvals.len();
while d > 0 {
d -= 1;
cvals[d] += 1;
if cvals[d] <= chi[d] {
return true;
}
cvals[d] = clo[d];
}
false
}
pub(super) fn eval_vec<'a>(
expr: &Expr,
bx: &VecBox,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
ops: &mut usize,
) -> Option<VecValue<'a>> {
*ops += 1;
let cse_class = ctx.cse.and_then(|c| c.class_of(expr));
if let (Some(rt), Some(class)) = (ctx.cse, cse_class)
&& let Some(hit) = rt.get(class, bx)
{
return Some(hit);
}
let r = match expr {
Expr::Number(n) => Some(VecValue::Scalar(*n)),
Expr::Integer(n) => Some(VecValue::Scalar(*n as f64)),
Expr::Variable(name) => eval_vec_variable(name, bx, ctx, pool),
Expr::Operator(node) => eval_vec_op(node, bx, ctx, pool, ops),
};
match (ctx.cse, cse_class, r) {
(Some(rt), Some(class), Some(v)) => Some(rt.put(class, v, bx)),
(_, _, r) => r,
}
}
pub(super) fn eval_vec_variable<'a>(
name: &str,
bx: &VecBox,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
) -> Option<VecValue<'a>> {
if name == "t" {
return Some(VecValue::Scalar(ctx.t));
}
if let Some(v) = bx.cbind(name) {
return Some(VecValue::Scalar(v as f64));
}
if let Some(a) = bx.syms.iter().position(|s| s == name) {
let mut ramp = pool.take_array_uninit(bx.shape);
let lo = bx.lo[a];
ramp.indexed_iter_mut()
.for_each(|(idx, v)| *v = (lo + idx[a] as i64) as f64);
return Some(VecValue::Owned {
data: ramp,
origin: bx.lo.iter().copied().collect(),
});
}
if let Some(a) = ctx.state_arrays.get(name) {
return Some(if a.ndim() == 0 {
VecValue::Scalar(a[IxDyn(&[])])
} else {
VecValue::View {
data: a.view(),
origin: DimI::from_elem(1, a.ndim()),
}
});
}
if let Some(a) = ctx.observed_arrays.get(name) {
return Some(if a.ndim() == 0 {
VecValue::Scalar(a[IxDyn(&[])])
} else {
VecValue::View {
data: a.view(),
origin: DimI::from_elem(1, a.ndim()),
}
});
}
if let Some(i) = ctx.param_names.iter().position(|p| p == name) {
return Some(VecValue::Scalar(ctx.params[i]));
}
None
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(super) enum VecOp {
Arith(BinCode),
Cmp(BinCode),
Unary(UnCode),
Neg,
Index,
Aggregate,
Makearray,
Const,
BoolLit(bool),
Ifelse,
Broadcast,
Precision,
Unsupported,
}
pub(super) fn vec_op_code(op: &str) -> VecOp {
match op {
crate::precision_infer::MARKER_OP => VecOp::Precision,
"+" => VecOp::Arith(BinCode::Add),
"-" => VecOp::Arith(BinCode::Sub),
"*" => VecOp::Arith(BinCode::Mul),
"/" => VecOp::Arith(BinCode::Div),
"^" | "pow" => VecOp::Arith(BinCode::Pow),
"min" => VecOp::Arith(BinCode::Min),
"max" => VecOp::Arith(BinCode::Max),
"atan2" => VecOp::Arith(BinCode::Atan2),
"and" => VecOp::Arith(BinCode::And),
"or" => VecOp::Arith(BinCode::Or),
"==" => VecOp::Cmp(BinCode::Eq),
"!=" => VecOp::Cmp(BinCode::Ne),
"<" => VecOp::Cmp(BinCode::Lt),
"<=" => VecOp::Cmp(BinCode::Le),
">" => VecOp::Cmp(BinCode::Gt),
">=" => VecOp::Cmp(BinCode::Ge),
"exp" => VecOp::Unary(UnCode::Exp),
"log" => VecOp::Unary(UnCode::Ln),
"log10" => VecOp::Unary(UnCode::Log10),
"sqrt" => VecOp::Unary(UnCode::Sqrt),
"abs" => VecOp::Unary(UnCode::Abs),
"sign" => VecOp::Unary(UnCode::Sign),
"floor" => VecOp::Unary(UnCode::Floor),
"ceil" => VecOp::Unary(UnCode::Ceil),
"sin" => VecOp::Unary(UnCode::Sin),
"cos" => VecOp::Unary(UnCode::Cos),
"tan" => VecOp::Unary(UnCode::Tan),
"asin" => VecOp::Unary(UnCode::Asin),
"acos" => VecOp::Unary(UnCode::Acos),
"atan" => VecOp::Unary(UnCode::Atan),
"sinh" => VecOp::Unary(UnCode::Sinh),
"cosh" => VecOp::Unary(UnCode::Cosh),
"tanh" => VecOp::Unary(UnCode::Tanh),
"asinh" => VecOp::Unary(UnCode::Asinh),
"acosh" => VecOp::Unary(UnCode::Acosh),
"atanh" => VecOp::Unary(UnCode::Atanh),
"not" => VecOp::Unary(UnCode::Not),
"neg" => VecOp::Neg,
"index" => VecOp::Index,
"faq" => VecOp::Aggregate,
"makearray" => VecOp::Makearray,
"const" => VecOp::Const,
"true" => VecOp::BoolLit(true),
"false" => VecOp::BoolLit(false),
"ifelse" => VecOp::Ifelse,
"broadcast" => VecOp::Broadcast,
_ => VecOp::Unsupported,
}
}
pub(super) fn eval_vec_op<'a>(
node: &ExpressionNode,
bx: &VecBox,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
ops: &mut usize,
) -> Option<VecValue<'a>> {
eval_vec_op_code(vec_op_code(&node.op), node, bx, ctx, pool, ops)
}
fn eval_vec_op_code<'a>(
code: VecOp,
node: &ExpressionNode,
bx: &VecBox,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
ops: &mut usize,
) -> Option<VecValue<'a>> {
match code {
VecOp::Precision => {
let arg = node.args.first()?;
let p = crate::precision_infer::marker_precision(node)?;
let _guard = crate::precision::enter(p);
eval_vec(arg, bx, ctx, pool, ops)
}
VecOp::Arith(code) => {
let (first, rest) = node.args.split_first()?;
if code == BinCode::Sub && rest.is_empty() {
return Some(vec_negate(eval_vec(first, bx, ctx, pool, ops)?, pool));
}
let mut acc = eval_vec(first, bx, ctx, pool, ops)?;
for a in rest {
let v = eval_vec(a, bx, ctx, pool, ops)?;
acc = vec_combine(code, acc, v, pool)?;
}
Some(acc)
}
VecOp::Neg => Some(vec_negate(
eval_vec(node.args.first()?, bx, ctx, pool, ops)?,
pool,
)),
VecOp::Index => eval_vec_index(node, bx, ctx, pool, ops),
VecOp::Aggregate => eval_vec_nested_aggregate(node, bx, ctx, pool, ops),
VecOp::Makearray => eval_vec_makearray(node, bx, ctx, pool, ops),
VecOp::Const => match eval_const(node) {
Value::Scalar(s) => Some(VecValue::Scalar(s)),
Value::Array(_) => None,
},
VecOp::BoolLit(b) => Some(VecValue::Scalar(if b { 1.0 } else { 0.0 })),
VecOp::Cmp(code) => {
if node.args.len() != 2 {
return None;
}
let a = eval_vec(&node.args[0], bx, ctx, pool, ops)?;
let b = eval_vec(&node.args[1], bx, ctx, pool, ops)?;
vec_combine(code, a, b, pool)
}
VecOp::Ifelse => {
if node.args.len() != 3 {
return None;
}
let cond = eval_vec(&node.args[0], bx, ctx, pool, ops)?;
match cond {
VecValue::Scalar(c) => {
if c != 0.0 {
eval_vec(&node.args[1], bx, ctx, pool, ops)
} else {
eval_vec(&node.args[2], bx, ctx, pool, ops)
}
}
cond_arr => {
let a = eval_vec(&node.args[1], bx, ctx, pool, ops)?;
let b = eval_vec(&node.args[2], bx, ctx, pool, ops)?;
vec_select(cond_arr, a, b, pool)
}
}
}
VecOp::Unary(code) => {
if node.args.len() != 1 {
return None;
}
let v = eval_vec(&node.args[0], bx, ctx, pool, ops)?;
Some(vec_unary(code, v, pool))
}
VecOp::Broadcast => {
let fn_name = node.broadcast_fn.as_deref()?;
if !crate::op_registry::is_scalar_operator(fn_name) {
return None;
}
eval_vec_op_code(vec_op_code(fn_name), node, bx, ctx, pool, ops)
}
VecOp::Unsupported => None,
}
}
pub(super) fn expr_mentions(expr: &Expr, name: &str) -> bool {
match expr {
Expr::Variable(v) => v == name,
Expr::Number(_) | Expr::Integer(_) => false,
Expr::Operator(n) => {
n.args.iter().any(|a| expr_mentions(a, name))
|| n.expr.as_deref().is_some_and(|e| expr_mentions(e, name))
|| n.filter.as_deref().is_some_and(|e| expr_mentions(e, name))
|| n.lower.as_deref().is_some_and(|e| expr_mentions(e, name))
|| n.upper.as_deref().is_some_and(|e| expr_mentions(e, name))
|| n.values
.as_ref()
.is_some_and(|vs| vs.iter().any(|e| expr_mentions(e, name)))
}
}
}
pub(super) fn nested_aggregate_capture<'n>(
spec: &ArrayOpSpec<'_>,
enclosing: impl Iterator<Item = &'n String>,
) -> Option<&'n String> {
for name in enclosing {
if spec.idx_names.iter().any(|n| n == name) || spec.contract_names.iter().any(|n| n == name)
{
continue;
}
if expr_mentions(spec.body, name) || spec.filter.is_some_and(|f| expr_mentions(f, name)) {
return Some(name);
}
}
None
}
fn eval_vec_nested_aggregate<'a>(
node: &ExpressionNode,
bx: &VecBox,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
ops: &mut usize,
) -> Option<VecValue<'a>> {
let spec = match faq_spec(node) {
Some(s) => s,
None => bail_vec!("aggregate: node carries no `expr` body"),
};
if spec.ranges.is_empty() {
bail_vec!("aggregate: rank-0 output (scalar reduction)");
}
if spec.has_drivable_overlap() {
bail_vec!("aggregate: carries an overlap join gate that drives enumeration");
}
if nested_aggregate_capture(
&spec,
bx.syms
.iter()
.chain(bx.cnames.iter())
.chain(ctx.loop_binds.keys()),
)
.is_some()
{
bail_vec!("aggregate: nested body depends on an enclosing bound index");
}
let (v, sub_ops) = try_eval_faq_vectorized(
spec.idx_names,
&spec.ranges,
spec.body,
&spec.contract_names,
&spec.contract_dims,
spec.reduce,
spec.filter,
ctx,
pool,
)?;
*ops += sub_ops;
Some(v)
}
pub(super) fn scalar_compare(op: &str, a: f64, b: f64) -> f64 {
let t = match op {
"==" => a == b,
"!=" => a != b,
"<" => a < b,
"<=" => a <= b,
">" => a > b,
">=" => a >= b,
_ => return f64::NAN,
};
if t { 1.0 } else { 0.0 }
}
pub(super) fn vec_negate<'a>(v: VecValue<'a>, pool: &mut Pool) -> VecValue<'a> {
match v {
VecValue::Scalar(s) => VecValue::Scalar(-s),
VecValue::Owned { mut data, origin } => {
data.mapv_inplace(|x| -x);
VecValue::Owned { data, origin }
}
VecValue::View { data, origin } => {
let mut buf = pool.take_array_uninit(data.shape());
ndarray::Zip::from(&mut buf)
.and(data)
.for_each(|o, &x| *o = -x);
VecValue::Owned { data: buf, origin }
}
}
}
pub(super) fn vec_unary<'a>(op: UnCode, v: VecValue<'a>, pool: &mut Pool) -> VecValue<'a> {
let f = unary_kernel_of(op);
match v {
VecValue::Scalar(s) => VecValue::Scalar(f(s)),
VecValue::Owned { mut data, origin } => {
data.mapv_inplace(f);
VecValue::Owned { data, origin }
}
VecValue::View { data, origin } => {
let mut buf = pool.take_array_uninit(data.shape());
ndarray::Zip::from(&mut buf)
.and(data)
.for_each(|o, &x| *o = f(x));
VecValue::Owned { data: buf, origin }
}
}
}
pub(super) fn vec_combine<'a>(
op: BinCode,
a: VecValue<'a>,
b: VecValue<'a>,
pool: &mut Pool,
) -> Option<VecValue<'a>> {
if crate::precision::is_f32() {
return vec_combine_with(binary_kernel_of(op), a, b, pool);
}
match op {
BinCode::Add => vec_combine_with(|x, y| x + y, a, b, pool),
BinCode::Sub => vec_combine_with(|x, y| x - y, a, b, pool),
BinCode::Mul => vec_combine_with(|x, y| x * y, a, b, pool),
BinCode::Div => vec_combine_with(|x, y| x / y, a, b, pool),
BinCode::Pow => vec_combine_with(|x: f64, y: f64| x.powf(y), a, b, pool),
BinCode::Min => vec_combine_with(|x: f64, y: f64| x.min(y), a, b, pool),
BinCode::Max => vec_combine_with(|x: f64, y: f64| x.max(y), a, b, pool),
other => vec_combine_with(binary_kernel_of(other), a, b, pool),
}
}
fn vec_combine_with<'a, F: Fn(f64, f64) -> f64>(
f: F,
a: VecValue<'a>,
b: VecValue<'a>,
pool: &mut Pool,
) -> Option<VecValue<'a>> {
match (a, b) {
(VecValue::Scalar(x), VecValue::Scalar(y)) => Some(VecValue::Scalar(f(x, y))),
(VecValue::Scalar(x), barr) => {
let (mut data, origin) = barr.into_owned(pool);
data.mapv_inplace(|y| f(x, y));
Some(VecValue::Owned { data, origin })
}
(aarr, VecValue::Scalar(y)) => {
let (mut data, origin) = aarr.into_owned(pool);
data.mapv_inplace(|x| f(x, y));
Some(VecValue::Owned { data, origin })
}
(aarr, barr) => {
let same = aarr.origin() == barr.origin() && aarr.shape() == barr.shape();
if !same {
aarr.release(pool);
barr.release(pool);
return None;
}
match (aarr, barr) {
(VecValue::Owned { mut data, origin }, b2) => {
{
let bv = b2.view().expect("array operand has a view");
ndarray::Zip::from(&mut data)
.and(&bv)
.for_each(|x, &y| *x = f(*x, y));
}
b2.release(pool);
Some(VecValue::Owned { data, origin })
}
(a2, VecValue::Owned { mut data, origin }) => {
let av = a2.view().expect("array operand has a view");
ndarray::Zip::from(&mut data)
.and(&av)
.for_each(|bslot, &aval| *bslot = f(aval, *bslot));
Some(VecValue::Owned { data, origin })
}
(a2, b2) => {
let origin: DimI = a2.origin().expect("array origin").iter().copied().collect();
let av = a2.view().expect("array operand has a view");
let bv = b2.view().expect("array operand has a view");
let mut buf = pool.take_array_uninit(av.shape());
ndarray::Zip::from(&mut buf)
.and(&av)
.and(&bv)
.for_each(|o, &x, &y| *o = f(x, y));
Some(VecValue::Owned { data: buf, origin })
}
}
}
}
}
pub(super) fn vec_select<'a>(
cond: VecValue<'a>,
a: VecValue<'a>,
b: VecValue<'a>,
pool: &mut Pool,
) -> Option<VecValue<'a>> {
let (cond_data, origin) = cond.into_owned(pool);
let shp: DimU = cond_data.shape().iter().copied().collect();
let box_ok = |v: &VecValue| match v.shape() {
None => true,
Some(s) => s == &shp[..] && v.origin() == Some(&origin[..]),
};
if !box_ok(&a) || !box_ok(&b) {
pool.give_array(cond_data);
a.release(pool);
b.release(pool);
return None;
}
let fill = |v: VecValue<'a>, pool: &mut Pool| -> ArrayD<f64> {
match v {
VecValue::Scalar(s) => {
let mut buf = pool.take_array_uninit(&shp);
buf.fill(s);
buf
}
other => other.into_owned(pool).0,
}
};
let a_arr = fill(a, pool);
let b_arr = fill(b, pool);
let mut out = pool.take_array_uninit(&shp);
ndarray::Zip::from(&mut out)
.and(&cond_data)
.and(&a_arr)
.and(&b_arr)
.for_each(|o, &c, &av, &bv| *o = if c != 0.0 { av } else { bv });
pool.give_array(cond_data);
pool.give_array(a_arr);
pool.give_array(b_arr);
Some(VecValue::Owned { data: out, origin })
}
#[cfg(test)]
fn const_index_value(e: &Expr, bx: &VecBox) -> Option<i64> {
match affine_terms(e, "\u{0}", bx) {
Some((0, k)) => Some(k),
_ => None,
}
}
pub(super) fn eval_vec_index<'a>(
node: &ExpressionNode,
bx: &VecBox,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
ops: &mut usize,
) -> Option<VecValue<'a>> {
if node.args.is_empty() {
bail_vec!("index: no arguments");
}
let const_base = ctx.const_arrays.is_const_base(&node.args[0]);
let arg0 = match &node.args[0] {
Expr::Operator(c) if c.op == "const" && is_inline_const_array(c) => {
match eval_const_memo(c, ctx.const_lits) {
Value::Array(a) => VecValue::Owned {
origin: DimI::from_elem(1, a.ndim()),
data: *a,
},
Value::Scalar(_) => bail_vec!("index: malformed inline `const` literal"),
}
}
base => eval_vec(base, bx, ctx, pool, ops)?,
};
let n = node.args.len() - 1;
if arg0.shape().is_none() {
return match arg0 {
VecValue::Scalar(s) if n == 0 => Some(VecValue::Scalar(s)),
_ => None,
};
}
let src_ndim = arg0.shape().expect("array").len();
if n != src_ndim {
arg0.release(pool);
bail_vec!("index: arg count != source rank");
}
let src_origin: DimI = arg0
.origin()
.expect("array origin")
.iter()
.copied()
.collect();
let src_shape: DimU = arg0.shape().expect("array").iter().copied().collect();
let out_ndim = bx.shape.len();
let mut mapped: SmallVec<[Option<(usize, AxisIndex)>; 4]> =
(0..out_ndim).map(|_| None).collect();
let mut n_mapped = 0usize;
let mut fixed: SmallVec<[(usize, i64); 4]> = SmallVec::new();
let mut any_fixed_oob = false;
for d in 0..n {
let e = &node.args[1 + d];
match classify_axis_role(e, bx) {
Some(AxisRole::Map { out_axis, ax }) if mapped[out_axis].is_none() => {
mapped[out_axis] = Some((d, ax));
n_mapped += 1;
}
Some(AxisRole::Const(idx1)) => {
let i0 = idx1 - src_origin[d];
if i0 < 0 || i0 >= src_shape[d] as i64 {
any_fixed_oob = true;
fixed.push((d, 0));
} else {
fixed.push((d, i0));
}
}
_ => return eval_vec_data_gather(node, bx, ctx, pool, ops, arg0, const_base),
}
}
if any_fixed_oob {
arg0.release(pool);
if const_base {
bail_vec!("index: const-array gather with an out-of-range fixed axis (§5.5.5)");
}
return Some(if n_mapped == 0 {
VecValue::Scalar(0.0)
} else {
VecValue::Owned {
data: pool.take_array(bx.shape),
origin: bx.lo.iter().copied().collect(),
}
});
}
if n_mapped == 0 {
let mut idx = DimU::from_elem(0usize, n);
for &(d, i0) in &fixed {
idx[d] = i0 as usize;
}
let v = arg0.view().expect("array")[IxDyn(&idx[..])];
arg0.release(pool);
return Some(VecValue::Scalar(v));
}
let mut axis_segs: SmallVec<[SmallVec<[(usize, usize, usize); 2]>; 4]> = SmallVec::new();
for a in 0..out_ndim {
let Some((orig_d, ax)) = &mapped[a] else {
let mut segs = SmallVec::new();
segs.push((0usize, bx.shape[a], 0usize));
axis_segs.push(segs);
continue;
};
let so = src_origin[*orig_d];
let ssz = src_shape[*orig_d] as i64;
match ax {
AxisIndex::Affine(k) => {
let k = *k;
let lo_p = (so - bx.lo[a] - k).max(0);
let hi_p = (so + ssz - bx.lo[a] - k).min(bx.shape[a] as i64); if lo_p >= hi_p {
arg0.release(pool);
if const_base {
bail_vec!(
"index: const-array gather entirely out of range on an axis (§5.5.5)"
);
}
return Some(VecValue::Owned {
data: pool.take_array(bx.shape),
origin: bx.lo.iter().copied().collect(),
});
}
if const_base && (lo_p != 0 || hi_p != bx.shape[a] as i64) {
arg0.release(pool);
bail_vec!(
"index: const-array gather partially out of range on an axis (§5.5.5)"
);
}
let mut segs = SmallVec::new();
segs.push((
lo_p as usize,
(hi_p - lo_p) as usize,
(bx.lo[a] + lo_p + k - so) as usize,
));
axis_segs.push(segs);
}
AxisIndex::Wrap { k, period, lo } => {
let (k, period) = (*k, *period);
if so != bx.lo[a] || *lo != so || ssz != period || bx.shape[a] as i64 != period {
arg0.release(pool);
bail_vec!("index: periodic wrap axis is not a full-period roll");
}
let p = period as usize;
let s = (((k % period) + period) % period) as usize; let mut segs = SmallVec::new();
if s == 0 {
segs.push((0usize, p, 0usize));
} else {
segs.push((0usize, p - s, s));
segs.push((p - s, s, 0usize));
}
axis_segs.push(segs);
}
}
}
if !bx.cnames.is_empty()
&& n_mapped == out_ndim
&& (0..out_ndim).all(|a| {
axis_segs[a].len() == 1 && axis_segs[a][0].0 == 0 && axis_segs[a][0].1 == bx.shape[a]
})
&& let VecValue::View { data, .. } = &arg0
{
let mut sv = data.clone();
let mut fixed_desc: SmallVec<[(usize, usize); 4]> =
fixed.iter().map(|&(d, i0)| (d, i0 as usize)).collect();
fixed_desc.sort_by_key(|x| std::cmp::Reverse(x.0));
for (d, i0) in fixed_desc {
sv = sv.index_axis_move(ndarray::Axis(d), i0);
}
let mut mapped_src: SmallVec<[usize; 4]> =
mapped.iter().flatten().map(|(d, _)| *d).collect();
mapped_src.sort_unstable();
let perm: SmallVec<[usize; 4]> = (0..out_ndim)
.filter_map(|a| mapped[a].as_ref())
.map(|(d, _)| {
mapped_src
.iter()
.position(|s| s == d)
.expect("mapped source axis is in mapped_src")
})
.collect();
let mut sv = sv.permuted_axes(IxDyn(&perm[..]));
for a in 0..out_ndim {
let (_, l, s) = axis_segs[a][0];
sv.slice_axis_inplace(ndarray::Axis(a), Slice::from(s..s + l));
}
return Some(VecValue::View {
data: sv,
origin: bx.lo.iter().copied().collect(),
});
}
let mut fixed_desc: SmallVec<[(usize, usize); 4]> =
fixed.iter().map(|&(d, i0)| (d, i0 as usize)).collect();
fixed_desc.sort_by_key(|x| std::cmp::Reverse(x.0));
let mut rv = arg0.view().expect("array");
for (d, i0) in fixed_desc {
rv = rv.index_axis_move(ndarray::Axis(d), i0);
}
let mut mapped_src: SmallVec<[usize; 4]> = mapped.iter().flatten().map(|(d, _)| *d).collect();
mapped_src.sort_unstable();
let perm: SmallVec<[usize; 4]> = (0..out_ndim)
.filter_map(|a| mapped[a].as_ref())
.map(|(d, _)| {
mapped_src
.iter()
.position(|s| s == d)
.expect("mapped source axis is in mapped_src")
})
.collect();
let mut rv = rv.permuted_axes(IxDyn(&perm[..]));
for a in 0..out_ndim {
if mapped[a].is_none() {
rv = rv.insert_axis(ndarray::Axis(a));
}
}
let bshape: DimU = (0..out_ndim)
.map(|a| {
if mapped[a].is_some() {
rv.shape()[a]
} else {
bx.shape[a]
}
})
.collect();
let rvb = match rv.broadcast(IxDyn(&bshape[..])) {
Some(v) => v,
None => {
drop(rv);
arg0.release(pool);
bail_vec!("index: broadcast to the output box failed");
}
};
let mut result = pool.take_array(bx.shape);
{
let mut pick = DimU::from_elem(0usize, out_ndim);
loop {
{
let mut out_view = result.slice_each_axis_mut(|ax| {
let d = ax.axis.index();
let (o, l, _) = axis_segs[d][pick[d]];
Slice::from(o..o + l)
});
let src_sub = rvb.slice_each_axis(|ax| {
let d = ax.axis.index();
let (_, l, s) = axis_segs[d][pick[d]];
Slice::from(s..s + l)
});
out_view.assign(&src_sub);
}
let mut d = 0;
let mut done = false;
loop {
if d == out_ndim {
done = true;
break;
}
pick[d] += 1;
if pick[d] < axis_segs[d].len() {
break;
}
pick[d] = 0;
d += 1;
}
if done {
break;
}
}
}
drop(rvb);
drop(rv);
arg0.release(pool);
Some(VecValue::Owned {
data: result,
origin: bx.lo.iter().copied().collect(),
})
}
fn eval_vec_data_gather<'a>(
node: &ExpressionNode,
bx: &VecBox,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
ops: &mut usize,
arg0: VecValue<'a>,
const_base: bool,
) -> Option<VecValue<'a>> {
use crate::value_invention::BoundaryKind;
let src_shape: DimU = arg0.shape().expect("array").iter().copied().collect();
if arg0.origin().expect("array").iter().any(|&o| o != 1) {
arg0.release(pool);
bail_vec!("index: computed-subscript read of a source not at origin 1");
}
let name = match &node.args[0] {
Expr::Variable(v) => v.as_str(),
_ => INLINE_CONST_NAME,
};
let policy: SmallVec<[Option<BoundaryKind>; 4]> = (0..src_shape.len())
.map(|d| {
const_base.then(|| {
if src_shape[d] == 0 {
BoundaryKind::Error
} else {
ctx.const_arrays.boundary(name, d)
}
})
})
.collect();
let mut subs: SmallVec<[VecValue<'a>; 4]> = SmallVec::new();
for e in &node.args[1..] {
let Some(v) = eval_vec(e, bx, ctx, pool, ops) else {
for s in subs {
s.release(pool);
}
arg0.release(pool);
return None;
};
let fits = match (v.shape(), v.origin()) {
(None, _) => true,
(Some(s), Some(o)) => s == bx.shape && o == bx.lo,
_ => false,
};
subs.push(v);
if !fits {
for s in subs {
s.release(pool);
}
arg0.release(pool);
bail_vec!("index: a computed subscript's box is not the output box");
}
}
let mut result = pool.take_array_uninit(bx.shape);
let mut ok = true;
{
let src = arg0.view().expect("array");
let views: SmallVec<[(Option<ArrayViewD<'_, f64>>, f64); 4]> = subs
.iter()
.map(|s| match s {
VecValue::Scalar(x) => (None, *x),
other => (other.view(), 0.0),
})
.collect();
let mut at: DimU = DimU::from_elem(0, src_shape.len());
'cells: for (cell, out) in result.indexed_iter_mut() {
let mut ghost = false;
for (d, (view, scalar)) in views.iter().enumerate() {
let v = match view {
Some(a) => a[&cell],
None => *scalar,
};
let r = v.round() as i64;
let n_d = src_shape[d] as i64;
at[d] = if (1..=n_d).contains(&r) {
(r - 1) as usize
} else {
match policy[d] {
None => {
ghost = true;
0
}
Some(BoundaryKind::Periodic) if n_d >= 1 => {
(r - 1).rem_euclid(n_d) as usize
}
Some(BoundaryKind::Clamp) if n_d >= 1 => (r.clamp(1, n_d) - 1) as usize,
Some(_) => {
ok = false;
break 'cells;
}
}
};
}
*out = if ghost { 0.0 } else { src[&at[..]] };
}
}
*ops += 1;
for s in subs {
s.release(pool);
}
arg0.release(pool);
if !ok {
pool.give_array(result);
bail_vec!("index: const-array gather out of range (§5.5.5)");
}
Some(VecValue::Owned {
data: result,
origin: bx.lo.iter().copied().collect(),
})
}
pub(super) fn region_value_axes(value: &[usize], region: &[usize]) -> Option<SmallVec<[bool; 4]>> {
if value == region {
return Some(SmallVec::from_elem(true, region.len()));
}
let covered: SmallVec<[bool; 4]> = region.iter().map(|&n| n != 1).collect();
let extents: SmallVec<[usize; 4]> = region.iter().copied().filter(|&n| n != 1).collect();
(value.len() < region.len() && value == extents.as_slice()).then_some(covered)
}
pub(super) fn region_value_view<'v>(
value: ndarray::ArrayViewD<'v, f64>,
covered: &[bool],
) -> ndarray::ArrayViewD<'v, f64> {
let mut v = value;
for (a, &c) in covered.iter().enumerate() {
if !c {
v = v.insert_axis(ndarray::Axis(a));
}
}
v
}
pub(super) fn eval_vec_makearray<'a>(
node: &ExpressionNode,
bx: &VecBox,
ctx: &EvalCtx<'a>,
pool: &mut Pool,
ops: &mut usize,
) -> Option<VecValue<'a>> {
let regions = match node.regions.as_ref() {
Some(r) => r,
None => bail_vec!("makearray: no `regions`"),
};
let values = match node.values.as_ref() {
Some(v) => v,
None => bail_vec!("makearray: no `values`"),
};
if regions.is_empty() || values.len() != regions.len() {
bail_vec!("makearray: empty or mismatched regions/values");
}
let ndim = regions[0].len();
if ndim != bx.shape.len() {
bail_vec!("makearray: region rank != output box rank");
}
let mut lo_bb = DimI::from_elem(i64::MAX, ndim);
let mut hi_bb = DimI::from_elem(i64::MIN, ndim);
for region in regions {
if region.len() != ndim {
return None;
}
for (d, r) in region.iter().enumerate() {
let Some([r_lo, r_hi]) = crate::types::region_bounds(r) else {
bail_vec!("makearray: unfolded (symbolic) region bound");
};
lo_bb[d] = lo_bb[d].min(r_lo);
hi_bb[d] = hi_bb[d].max(r_hi);
}
}
let bb_shape: DimU = (0..ndim)
.map(|d| (hi_bb[d] - lo_bb[d] + 1) as usize)
.collect();
let mut result = pool.take_array(&bb_shape);
for (region, value_expr) in regions.iter().zip(values.iter()) {
if region
.iter()
.any(|r| crate::types::region_bounds(r).is_none())
{
pool.give_array(result);
bail_vec!("makearray: unfolded (symbolic) region bound");
}
let r_lo: DimI = region.iter().map(|r| r[0].as_i64().unwrap_or(0)).collect();
let r_shape: DimU = region
.iter()
.map(|r| {
let [lo, hi] = crate::types::region_bounds(r).unwrap_or([0, -1]);
(hi - lo + 1) as usize
})
.collect();
if r_shape.contains(&0) {
pool.give_array(result);
bail_vec!("makearray: empty region");
}
let _cse_scope = ctx.cse.map(|rt| rt.scope(pool));
let rbx = VecBox {
syms: bx.syms,
lo: &r_lo[..],
shape: &r_shape[..],
cnames: bx.cnames,
cvals: bx.cvals,
};
let v = match eval_vec(value_expr, &rbx, ctx, pool, ops) {
Some(v) => v,
None => {
pool.give_array(result);
return None;
}
};
let covered = match (v.shape(), v.origin()) {
(None, _) => None, (Some(s), Some(o)) => match region_value_axes(s, &r_shape) {
Some(c)
if o.iter()
.eq((0..r_shape.len()).filter(|&a| c[a]).map(|a| &r_lo[a])) =>
{
Some(c)
}
_ => {
v.release(pool);
pool.give_array(result);
return None;
}
},
(Some(_), None) => {
v.release(pool);
pool.give_array(result);
return None;
}
};
match v {
VecValue::Scalar(s) => {
let mut sub = result.slice_each_axis_mut(|ax| {
let d = ax.axis.index();
let s0 = (r_lo[d] - lo_bb[d]) as usize;
Slice::from(s0..s0 + r_shape[d])
});
sub.fill(s);
}
other => {
{
let vview = other.view().expect("array operand has a view");
let vview = region_value_view(vview, covered.as_deref().unwrap_or(&[]));
let mut sub = result.slice_each_axis_mut(|ax| {
let d = ax.axis.index();
let s0 = (r_lo[d] - lo_bb[d]) as usize;
Slice::from(s0..s0 + r_shape[d])
});
sub.assign(&vview);
}
other.release(pool);
}
}
}
Some(VecValue::Owned {
data: result,
origin: lo_bb,
})
}
pub(super) enum AxisIndex {
Affine(i64),
Wrap { k: i64, period: i64, lo: i64 },
}
pub(super) fn wrap_pieces(
out_lo: i64,
n: usize,
k: i64,
lo: i64,
period: i64,
) -> SmallVec<[(usize, usize, i64); 3]> {
let n_i = n as i64;
let hi = lo + period - 1;
let below = (lo - k - out_lo).clamp(0, n_i);
let above = (hi - k - out_lo + 1).clamp(below, n_i);
let mut pieces = SmallVec::new();
for (start, end, shift) in [
(0, below, k + period),
(below, above, k),
(above, n_i, k - period),
] {
if end > start {
pieces.push((start as usize, (end - start) as usize, shift));
}
}
pieces
}
#[cfg(test)]
pub(super) fn classify_axis_index(expr: &Expr, sym: &str, bx: &VecBox) -> Option<AxisIndex> {
if let Some(k) = affine_offset_in(expr, sym, bx) {
return Some(AxisIndex::Affine(k));
}
parse_wrap_axis(expr, sym, bx)
}
pub(super) fn affine_offset_in(expr: &Expr, sym: &str, bx: &VecBox) -> Option<i64> {
let (coeff, konst) = affine_terms(expr, sym, bx)?;
if coeff == 1 { Some(konst) } else { None }
}
#[inline(always)]
fn sym_eq(a: &str, b: &str) -> bool {
let (x, y) = (a.as_bytes(), b.as_bytes());
x.len() == y.len() && x.iter().zip(y).all(|(p, q)| p == q)
}
pub(super) enum AxisRole {
Map { out_axis: usize, ax: AxisIndex },
Const(i64),
}
pub(super) fn classify_axis_role(expr: &Expr, bx: &VecBox) -> Option<AxisRole> {
if let Some((sym, coeff, konst)) = affine_terms_any(expr, bx) {
return match (sym, coeff) {
(Some(out_axis), 1) => Some(AxisRole::Map {
out_axis,
ax: AxisIndex::Affine(konst),
}),
(None, 0) => Some(AxisRole::Const(konst)),
_ => None,
};
}
parse_wrap_axis_any(expr, bx).map(|(out_axis, ax)| AxisRole::Map { out_axis, ax })
}
pub(super) fn affine_terms_any(expr: &Expr, bx: &VecBox) -> Option<(Option<usize>, i64, i64)> {
#[inline]
fn join(a: Option<usize>, b: Option<usize>) -> Option<Option<usize>> {
match (a, b) {
(None, s) | (s, None) => Some(s),
(Some(x), Some(y)) if x == y => Some(Some(x)),
_ => None, }
}
match expr {
Expr::Integer(n) => Some((None, 0, *n)),
Expr::Number(n) if n.fract() == 0.0 => Some((None, 0, *n as i64)),
Expr::Number(_) => None,
Expr::Variable(v) => {
if let Some(a) = bx.syms.iter().position(|s| sym_eq(s, v)) {
Some((Some(a), 1, 0))
} else {
bx.cbind(v).map(|k| (None, 0, k))
}
}
Expr::Operator(node) => match node.op.as_str() {
"+" => {
let mut sym = None;
let mut coeff = 0i64;
let mut konst = 0i64;
for a in &node.args {
let (s, c, k) = affine_terms_any(a, bx)?;
sym = join(sym, s)?;
coeff = coeff.checked_add(c)?;
konst = konst.checked_add(k)?;
}
Some((sym, coeff, konst))
}
"-" if node.args.len() == 2 => {
let (s0, c0, k0) = affine_terms_any(&node.args[0], bx)?;
let (s1, c1, k1) = affine_terms_any(&node.args[1], bx)?;
Some((join(s0, s1)?, c0.checked_sub(c1)?, k0.checked_sub(k1)?))
}
"-" | "neg" if node.args.len() == 1 => {
let (s, c, k) = affine_terms_any(&node.args[0], bx)?;
Some((s, c.checked_neg()?, k.checked_neg()?))
}
"*" => {
let mut sym_factor: Option<(Option<usize>, i64, i64)> = None;
let mut m: i64 = 1;
for a in &node.args {
let (s, c, k) = affine_terms_any(a, bx)?;
if c != 0 {
if sym_factor.is_some() {
return None; }
sym_factor = Some((s, c, k));
} else {
if s.is_some() {
return None;
}
m = m.checked_mul(k)?;
}
}
match sym_factor {
Some((s, c, k)) => Some((s, c.checked_mul(m)?, k.checked_mul(m)?)),
None => Some((None, 0, m)),
}
}
_ => None,
},
}
}
pub(super) fn parse_wrap_axis_any(expr: &Expr, bx: &VecBox) -> Option<(usize, AxisIndex)> {
fn offset_in_axis(expr: &Expr, a: usize, bx: &VecBox) -> Option<i64> {
match affine_terms_any(expr, bx)? {
(Some(s), 1, k) if s == a => Some(k),
_ => None,
}
}
let outer = as_op(expr, "ifelse", 3)?;
let (lt_lhs, lo_bound) = as_cmp_const(&outer.args[0], "<")?;
let (sym, coeff, k) = affine_terms_any(lt_lhs, bx)?;
let a = sym?;
if coeff != 1 {
return None;
}
let p1 = offset_in_axis(&outer.args[1], a, bx)?.checked_sub(k)?;
let inner_if = as_op(&outer.args[2], "ifelse", 3)?;
let (gt_lhs, hi_bound) = as_cmp_const(&inner_if.args[0], ">")?;
if offset_in_axis(gt_lhs, a, bx)? != k {
return None;
}
let p2 = k.checked_sub(offset_in_axis(&inner_if.args[1], a, bx)?)?;
if offset_in_axis(&inner_if.args[2], a, bx)? != k {
return None; }
let period = hi_bound.checked_sub(lo_bound)?.checked_add(1)?;
if p1 != period || p2 != period || period <= 0 {
return None;
}
Some((
a,
AxisIndex::Wrap {
k,
period,
lo: lo_bound,
},
))
}
pub(super) fn affine_terms(expr: &Expr, sym: &str, bx: &VecBox) -> Option<(i64, i64)> {
match expr {
Expr::Integer(n) => Some((0, *n)),
Expr::Number(n) if n.fract() == 0.0 => Some((0, *n as i64)),
Expr::Number(_) => None,
Expr::Variable(v) if v == sym => Some((1, 0)),
Expr::Variable(v) => bx.cbind(v).map(|k| (0, k)),
Expr::Operator(node) => match node.op.as_str() {
"+" => {
let mut coeff = 0i64;
let mut konst = 0i64;
for a in &node.args {
let (c, k) = affine_terms(a, sym, bx)?;
coeff = coeff.checked_add(c)?;
konst = konst.checked_add(k)?;
}
Some((coeff, konst))
}
"-" if node.args.len() == 2 => {
let (c0, k0) = affine_terms(&node.args[0], sym, bx)?;
let (c1, k1) = affine_terms(&node.args[1], sym, bx)?;
Some((c0.checked_sub(c1)?, k0.checked_sub(k1)?))
}
"-" | "neg" if node.args.len() == 1 => {
let (c, k) = affine_terms(&node.args[0], sym, bx)?;
Some((c.checked_neg()?, k.checked_neg()?))
}
"*" => {
let mut sym_factor: Option<(i64, i64)> = None;
let mut m: i64 = 1;
for a in &node.args {
let (c, k) = affine_terms(a, sym, bx)?;
if c != 0 {
if sym_factor.is_some() {
return None; }
sym_factor = Some((c, k));
} else {
m = m.checked_mul(k)?;
}
}
match sym_factor {
Some((c, k)) => Some((c.checked_mul(m)?, k.checked_mul(m)?)),
None => Some((0, m)),
}
}
_ => None,
},
}
}
#[cfg(test)]
pub(super) fn parse_wrap_axis(expr: &Expr, sym: &str, bx: &VecBox) -> Option<AxisIndex> {
let outer = as_op(expr, "ifelse", 3)?;
let (lt_lhs, lo_bound) = as_cmp_const(&outer.args[0], "<")?;
let k = affine_offset_in(lt_lhs, sym, bx)?;
let p1 = affine_offset_in(&outer.args[1], sym, bx)?.checked_sub(k)?;
let inner_if = as_op(&outer.args[2], "ifelse", 3)?;
let (gt_lhs, hi_bound) = as_cmp_const(&inner_if.args[0], ">")?;
if affine_offset_in(gt_lhs, sym, bx)? != k {
return None;
}
let p2 = k.checked_sub(affine_offset_in(&inner_if.args[1], sym, bx)?)?;
if affine_offset_in(&inner_if.args[2], sym, bx)? != k {
return None; }
let period = hi_bound.checked_sub(lo_bound)?.checked_add(1)?;
if p1 != period || p2 != period || period <= 0 {
return None;
}
Some(AxisIndex::Wrap {
k,
period,
lo: lo_bound,
})
}
pub(super) fn as_op<'e>(expr: &'e Expr, op: &str, arity: usize) -> Option<&'e ExpressionNode> {
match expr {
Expr::Operator(node) if node.op == op && node.args.len() == arity => Some(node),
_ => None,
}
}
pub(super) fn as_cmp_const<'e>(expr: &'e Expr, op: &str) -> Option<(&'e Expr, i64)> {
let node = as_op(expr, op, 2)?;
let c = match &node.args[1] {
Expr::Integer(n) => *n,
Expr::Number(n) if n.fract() == 0.0 => *n as i64,
_ => return None,
};
Some((&node.args[0], c))
}
#[cfg(test)]
mod axis_classifier_equivalence {
use super::*;
use serde_json::json;
fn ex(v: serde_json::Value) -> Expr {
serde_json::from_value(v).expect("axis expression deserializes")
}
#[derive(Debug, PartialEq, Eq)]
enum Role {
Affine(usize, i64),
Wrap(usize, i64, i64, i64),
Const(i64),
Bail,
}
fn from_axis(a: usize, ax: &AxisIndex) -> Role {
match ax {
AxisIndex::Affine(k) => Role::Affine(a, *k),
AxisIndex::Wrap { k, period, lo } => Role::Wrap(a, *k, *period, *lo),
}
}
fn old_role(e: &Expr, bx: &VecBox, claimed: &[bool]) -> Role {
let hit = (0..bx.syms.len()).find_map(|a| {
if claimed[a] {
return None;
}
classify_axis_index(e, &bx.syms[a], bx).map(|ax| from_axis(a, &ax))
});
match hit {
Some(r) => r,
None => match const_index_value(e, bx) {
Some(k) => Role::Const(k),
None => Role::Bail,
},
}
}
fn new_role(e: &Expr, bx: &VecBox, claimed: &[bool]) -> Role {
match classify_axis_role(e, bx) {
Some(AxisRole::Map { out_axis, ax }) if !claimed[out_axis] => from_axis(out_axis, &ax),
Some(AxisRole::Const(k)) => Role::Const(k),
_ => Role::Bail,
}
}
fn wrap(inner: serde_json::Value, lo: i64, hi: i64) -> serde_json::Value {
let p = hi - lo + 1;
json!({"op": "ifelse", "args": [
{"op": "<", "args": [inner, lo]},
{"op": "+", "args": [inner, p]},
{"op": "ifelse", "args": [
{"op": ">", "args": [inner, hi]},
{"op": "-", "args": [inner, p]},
inner
]}
]})
}
#[test]
fn single_walk_classifier_matches_the_per_symbol_search() {
let syms: Vec<String> = ["i", "j", "k"].iter().map(|s| s.to_string()).collect();
let cnames: Vec<String> = ["m", "n"].iter().map(|s| s.to_string()).collect();
let lo = [1i64, 1, 1];
let shape = [4usize, 5, 6];
let cvals = [2i64, -1];
let bx = VecBox {
syms: &syms,
lo: &lo,
shape: &shape,
cnames: &cnames,
cvals: &cvals,
};
let cases: Vec<serde_json::Value> = vec![
json!("i"),
json!("j"),
json!("k"),
json!({"op": "+", "args": ["i", 1]}),
json!({"op": "-", "args": ["i", 1]}),
json!({"op": "+", "args": ["j", 3]}),
json!({"op": "-", "args": ["k", 2]}),
json!({"op": "+", "args": ["k", -2]}),
json!({"op": "+", "args": [{"op": "-", "args": ["j", 1]}, 4]}),
json!({"op": "neg", "args": [{"op": "neg", "args": ["j"]}]}),
json!("m"),
json!("n"),
json!({"op": "+", "args": ["i", "m"]}),
json!({"op": "+", "args": [{"op": "+", "args": ["k", 1]}, "n"]}),
json!({"op": "*", "args": ["m", 3]}),
json!({"op": "-", "args": ["m", "n"]}),
json!(1),
json!(7),
json!(2.0),
json!({"op": "+", "args": [2, 3]}),
json!({"op": "*", "args": [2, {"op": "+", "args": [3, 1]}]}),
json!({"op": "*", "args": [2, "i"]}),
json!({"op": "-", "args": ["i", "i"]}),
json!({"op": "+", "args": ["i", "j"]}),
json!({"op": "-", "args": ["k", "i"]}),
json!({"op": "*", "args": ["i", "j"]}),
json!({"op": "*", "args": ["i", "m"]}),
json!(2.5),
json!("q"),
json!({"op": "+", "args": ["i", "q"]}),
json!({"op": "/", "args": ["i", 2]}),
json!({"op": "sin", "args": ["i"]}),
wrap(json!({"op": "-", "args": ["i", 1]}), 1, 4),
wrap(json!({"op": "+", "args": ["i", 1]}), 1, 4),
wrap(json!("j"), 1, 5),
wrap(json!({"op": "+", "args": ["k", 2]}), 1, 6),
wrap(json!({"op": "+", "args": ["i", "m"]}), 1, 4),
wrap(json!({"op": "*", "args": [2, "i"]}), 1, 4),
wrap(json!({"op": "+", "args": ["i", "j"]}), 1, 4),
wrap(json!(3), 1, 4),
json!({"op": "ifelse", "args": [
{"op": "<", "args": ["i", 1]},
{"op": "+", "args": ["i", 4]},
{"op": "ifelse", "args": [
{"op": ">", "args": ["i", 4]},
{"op": "-", "args": ["i", 3]},
"i"
]}
]}),
json!({"op": "ifelse", "args": [
{"op": "<", "args": ["i", 1]},
{"op": "+", "args": ["i", 4]},
{"op": "ifelse", "args": [
{"op": ">", "args": ["i", 4]},
{"op": "-", "args": ["i", 4]},
{"op": "+", "args": ["i", 1]}
]}
]}),
];
for case in &cases {
let e = ex(case.clone());
for mask in 0u32..8 {
let claimed = [mask & 1 != 0, mask & 2 != 0, mask & 4 != 0];
let old = old_role(&e, &bx, &claimed);
let new = new_role(&e, &bx, &claimed);
assert_eq!(
old, new,
"axis expression {case} with claimed={claimed:?}: \
per-symbol search says {old:?}, single walk says {new:?}"
);
}
}
}
#[test]
fn single_walk_classifier_matches_on_a_rank_1_box() {
let syms: Vec<String> = vec!["i".to_string()];
let lo = [1i64];
let shape = [8usize];
let bx = VecBox {
syms: &syms,
lo: &lo,
shape: &shape,
cnames: &[],
cvals: &[],
};
let cases: Vec<serde_json::Value> = vec![
json!("i"),
json!({"op": "+", "args": ["i", 1]}),
json!({"op": "-", "args": ["i", 1]}),
json!(3),
json!("k"),
json!({"op": "*", "args": [2, "i"]}),
wrap(json!({"op": "-", "args": ["i", 1]}), 1, 8),
];
for case in &cases {
let e = ex(case.clone());
for mask in 0u32..2 {
let claimed = [mask & 1 != 0];
assert_eq!(
old_role(&e, &bx, &claimed),
new_role(&e, &bx, &claimed),
"axis expression {case} with claimed={claimed:?}"
);
}
}
}
}
#[cfg(test)]
mod op_dispatch_equivalence {
use super::*;
#[rustfmt::skip]
const ARITH: &[&str] = &["+", "-", "*", "/", "^", "min", "max", "atan2", "and", "or"];
#[rustfmt::skip]
const CMP: &[&str] = &["==", "!=", "<", "<=", ">", ">="];
#[rustfmt::skip]
const UNARY: &[&str] = &[
"exp", "log", "log10", "sqrt", "abs", "sign", "floor", "ceil", "sin",
"cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "asinh",
"acosh", "atanh", "not",
];
#[test]
fn vec_op_code_matches_the_dispatch_arms() {
for op in ARITH {
assert_eq!(
vec_op_code(op),
VecOp::Arith(BinCode::of(op)),
"arithmetic operator `{op}` lost its arm"
);
assert_ne!(BinCode::of(op), BinCode::Unknown, "`{op}` has no kernel");
}
for op in CMP {
assert_eq!(
vec_op_code(op),
VecOp::Cmp(BinCode::of(op)),
"comparison operator `{op}` lost its arm"
);
assert_ne!(BinCode::of(op), BinCode::Unknown, "`{op}` has no kernel");
}
for op in UNARY {
assert_eq!(
vec_op_code(op),
VecOp::Unary(UnCode::of(op)),
"unary operator `{op}` lost its arm"
);
assert_ne!(UnCode::of(op), UnCode::Unknown, "`{op}` has no kernel");
}
assert_eq!(vec_op_code("neg"), VecOp::Neg);
assert_eq!(vec_op_code("index"), VecOp::Index);
assert_eq!(vec_op_code("faq"), VecOp::Aggregate);
assert_eq!(vec_op_code("makearray"), VecOp::Makearray);
assert_eq!(vec_op_code("const"), VecOp::Const);
assert_eq!(vec_op_code("true"), VecOp::BoolLit(true));
assert_eq!(vec_op_code("false"), VecOp::BoolLit(false));
assert_eq!(vec_op_code("pow"), VecOp::Arith(BinCode::Pow));
assert_eq!(vec_op_code("ifelse"), VecOp::Ifelse);
assert_eq!(vec_op_code("broadcast"), VecOp::Broadcast);
}
#[test]
fn unvectorized_operators_still_bail() {
#[rustfmt::skip]
const OTHERS: &[&str] = &[
"D", "Pre", "fn", "arrayop", "reshape", "transpose", "concat",
"intersect_polygon", "polygon_intersection_area", "integral", "grad",
"skolem", "rank", "ic", "table_lookup", "apply_expression_template",
"", " ", "PLUS", "Log", "expp",
];
for op in OTHERS {
assert_eq!(
vec_op_code(op),
VecOp::Unsupported,
"operator `{op}` must fall back to the per-cell oracle"
);
}
}
#[test]
fn cse_box_transparent_ops_all_have_a_dispatch_arm() {
for op in super::super::cse::BOX_TRANSPARENT_OPS {
assert_ne!(
vec_op_code(op),
VecOp::Unsupported,
"`{op}` is CSE-box-transparent but has no `eval_vec_op` arm"
);
}
}
#[test]
fn every_box_transparent_dispatch_arm_is_listed() {
let listed = |op: &str| super::super::cse::BOX_TRANSPARENT_OPS.contains(&op);
for op in ARITH.iter().chain(CMP).chain(UNARY) {
assert!(
listed(op),
"`{op}` has a dispatch arm but is not CSE-transparent"
);
}
for op in ["neg", "index", "ifelse", "broadcast"] {
assert!(
listed(op),
"`{op}` has a dispatch arm but is not CSE-transparent"
);
}
for op in ["faq", "makearray"] {
assert!(
!listed(op),
"`{op}` rebinds the box and must not be CSE-transparent"
);
}
}
}