use std::sync::atomic::{AtomicU64, Ordering};
use crate::array::{Array, Data, Layout};
use crate::dtype::DType;
use crate::error::Span;
use crate::ir::{Expr, Program, Scope};
use crate::par;
use crate::simd::multiversioned;
use crate::verb::{
tol_cmp, windows_into, DyadOp, MonadOp, ScalarDyad, ScalarMonad, Tol, Verb, WindowKind,
RANK_INF,
};
pub const BLOCK: usize = 8_192;
pub const MAX_WINDOW: usize = 1_024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Instr {
Load(usize),
Monad(ScalarMonad),
Dyad(ScalarDyad),
Store(usize),
Let(usize),
Window(ScalarDyad, usize),
Scan(ScalarDyad),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Dom {
Result,
Wide,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct Plan {
window: Option<usize>,
scan: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Yield {
Values,
Reduce(ScalarDyad),
Tally,
}
#[derive(Clone, Debug)]
pub struct FusedKernel {
code: Vec<Instr>,
slots: usize,
yields: Yield,
leaves: Vec<usize>,
doms: Vec<Option<Dom>>,
let_doms: Vec<Dom>,
plan: Plan,
window: Option<usize>,
scans: usize,
tol: Tol,
}
impl FusedKernel {
pub fn code(&self) -> &[Instr] {
&self.code
}
pub fn yields(&self) -> Yield {
self.yields
}
pub fn reduce(&self) -> Option<ScalarDyad> {
match self.yields {
Yield::Reduce(op) => Some(op),
_ => None,
}
}
pub fn tol(&self) -> Tol {
self.tol
}
}
static FALLBACKS: AtomicU64 = AtomicU64::new(0);
pub fn fallback_count() -> u64 {
FALLBACKS.load(Ordering::Relaxed)
}
fn note_fallback() {
FALLBACKS.fetch_add(1, Ordering::Relaxed);
}
fn fusable_monad(v: &Verb) -> Option<ScalarMonad> {
use ScalarMonad::*;
let Verb::Prim(p) = v else { return None };
let MonadOp::Scalar(op) = p.monad else { return None };
matches!(
op,
Conj | Neg | Abs | Signum | Recip | Floor | Ceil | Inc | Dec | Double | Halve | Square
| OneMinus | Exp
)
.then_some(op)
}
fn fusable_dyad(v: &Verb) -> Option<ScalarDyad> {
use ScalarDyad::*;
let Verb::Prim(p) = v else { return None };
let DyadOp::Scalar(op) = p.dyad else { return None };
matches!(op, Add | Sub | Mul | DivJ | Min | Max | Residue | Eq | Ne | Lt | Le | Gt | Ge)
.then_some(op)
}
fn absorbable_reduce(v: &Verb) -> Option<ScalarDyad> {
use ScalarDyad::*;
let inner = match v {
Verb::Reduce(u) => u,
Verb::Rank(u, r) if r[0] >= 1 => match &**u {
Verb::Reduce(inner) => inner,
_ => return None,
},
_ => return None,
};
let Verb::Prim(p) = &**inner else { return None };
let DyadOp::Scalar(op) = p.dyad else { return None };
matches!(op, Add | Mul | Min | Max).then_some(op)
}
fn absorbable_window(e: &Expr) -> Option<(ScalarDyad, usize)> {
let Expr::Dyad { verb: Verb::Windowed(u, WindowKind::Prefix), x, .. } = e else {
return None;
};
let op = absorbable_reduce(u)?;
let Expr::Const(a, _) = &**x else { return None };
if a.rank() != 0 {
return None;
}
let k = *a.to_i64_vec()?.first()?;
(1..=MAX_WINDOW as i64).contains(&k).then_some((op, k as usize))
}
fn absorbable_scan(e: &Expr) -> Option<ScalarDyad> {
let Expr::Monad { verb, .. } = e else { return None };
let inner = match verb {
Verb::Rank(u, r) if r[0] >= 1 => &**u,
v => v,
};
let Verb::Windowed(u, kind) = inner else { return None };
if *kind == WindowKind::Suffix {
return None;
}
absorbable_reduce(u)
}
fn is_tally(v: &Verb) -> bool {
matches!(v, Verb::Prim(p) if p.monad == MonadOp::Tally && p.ranks[0] == RANK_INF)
}
#[derive(Clone, PartialEq)]
enum Node {
Leaf(usize),
Monad(ScalarMonad, Box<Node>),
Dyad(ScalarDyad, Box<Node>, Box<Node>),
Window(ScalarDyad, usize, Box<Node>),
Scan(ScalarDyad, Box<Node>),
}
#[derive(Default)]
struct Leaves<'a> {
inputs: Vec<&'a Expr>,
order: Vec<usize>,
doms: Vec<Option<Dom>>,
}
impl<'a> Leaves<'a> {
fn push(&mut self, e: &'a Expr, dom: Dom) -> usize {
let i = match self.inputs.iter().position(|&p| same(p, e)) {
Some(i) => i,
None => {
self.inputs.push(e);
self.doms.push(Some(dom));
self.inputs.len() - 1
}
};
if self.doms[i] != Some(dom) {
self.doms[i] = None;
}
self.order.push(i);
i
}
}
struct Inline<'a> {
name: &'a str,
def: &'a Expr,
hits: usize,
}
fn read_through<'a>(e: &Expr, sub: Option<&Inline<'a>>) -> Option<&'a Expr> {
match (e, sub) {
(Expr::Name(n, _), Some(s)) if n == s.name => Some(s.def),
_ => None,
}
}
fn plan_of(e: &Expr, sub: Option<&Inline<'_>>) -> Plan {
fn walk(e: &Expr, sub: Option<&Inline<'_>>, inside: bool, ks: &mut Vec<usize>, s: &mut bool) {
if let Some(def) = read_through(e, sub) {
return walk(def, sub, inside, ks, s);
}
match e {
Expr::Monad { verb, y, .. } if fusable_monad(verb).is_some() => {
walk(y, sub, inside, ks, s)
}
Expr::Dyad { verb, x, y, .. } if fusable_dyad(verb).is_some() => {
walk(y, sub, inside, ks, s);
walk(x, sub, inside, ks, s);
}
Expr::Dyad { y, .. } if !inside && absorbable_window(e).is_some() => {
ks.push(absorbable_window(e).expect("just matched").1);
walk(y, sub, true, ks, s);
}
Expr::Monad { y, .. } if absorbable_scan(e).is_some() => {
*s = true;
walk(y, sub, inside, ks, s);
}
_ => {}
}
}
let (mut ks, mut scan) = (Vec::new(), false);
walk(e, sub, false, &mut ks, &mut scan);
let window = match ks.split_first() {
Some((k, rest)) if rest.iter().all(|r| r == k) => Some(*k),
_ => None,
};
Plan { window, scan: window.is_none() && scan }
}
fn chain<'a>(
e: &'a Expr,
lv: &mut Leaves<'a>,
sub: &mut Option<Inline<'a>>,
plan: Plan,
dom: Dom,
) -> Node {
if read_through(e, sub.as_ref()).is_some() {
let def = read_through(e, sub.as_ref()).expect("just matched");
if let Some(s) = sub.as_mut() {
s.hits += 1;
}
return chain(def, lv, sub, plan, dom);
}
match e {
Expr::Monad { verb, y, .. } => {
if let Some(op) = fusable_monad(verb) {
return Node::Monad(op, Box::new(chain(y, lv, sub, plan, dom)));
}
if plan.scan && let Some(op) = absorbable_scan(e) {
return Node::Scan(op, Box::new(chain(y, lv, sub, plan, dom)));
}
Node::Leaf(lv.push(e, dom))
}
Expr::Dyad { verb, x, y, .. } => {
if let Some(op) = fusable_dyad(verb) {
let ry = chain(y, lv, sub, plan, dom);
let rx = chain(x, lv, sub, plan, dom);
return Node::Dyad(op, Box::new(rx), Box::new(ry));
}
if dom == Dom::Result
&& let Some((op, k)) = absorbable_window(e)
&& plan.window == Some(k)
{
return Node::Window(op, k, Box::new(chain(y, lv, sub, plan, Dom::Wide)));
}
Node::Leaf(lv.push(e, dom))
}
_ => Node::Leaf(lv.push(e, dom)),
}
}
fn ops(n: &Node) -> usize {
match n {
Node::Leaf(_) => 0,
Node::Monad(_, y) | Node::Window(_, _, y) | Node::Scan(_, y) => 1 + ops(y),
Node::Dyad(_, x, y) => 1 + ops(x) + ops(y),
}
}
fn subtrees<'a>(n: &'a Node, dom: Dom, out: &mut Vec<(&'a Node, Dom)>) {
if ops(n) == 0 {
return;
}
out.push((n, dom));
match n {
Node::Leaf(_) => {}
Node::Monad(_, y) | Node::Scan(_, y) => subtrees(y, dom, out),
Node::Window(_, _, y) => subtrees(y, Dom::Wide, out),
Node::Dyad(_, x, y) => {
subtrees(x, dom, out);
subtrees(y, dom, out);
}
}
}
fn lets_of(n: &Node) -> Vec<(Node, Dom)> {
let mut all = Vec::new();
subtrees(n, Dom::Result, &mut all);
let mut out = Vec::new();
fn walk(n: &Node, dom: Dom, all: &[(&Node, Dom)], out: &mut Vec<(Node, Dom)>) {
let count = |d: Dom| all.iter().filter(|(m, md)| *m == n && *md == d).count();
if ops(n) >= 1 && count(dom) >= 2 && count(other(dom)) == 0 {
if !out.iter().any(|(m, _)| m == n) {
out.push((n.clone(), dom));
}
return;
}
match n {
Node::Leaf(_) => {}
Node::Monad(_, y) | Node::Scan(_, y) => walk(y, dom, all, out),
Node::Window(_, _, y) => walk(y, Dom::Wide, all, out),
Node::Dyad(_, x, y) => {
walk(x, dom, all, out);
walk(y, dom, all, out);
}
}
}
walk(n, Dom::Result, &all, &mut out);
out
}
fn other(d: Dom) -> Dom {
match d {
Dom::Result => Dom::Wide,
Dom::Wide => Dom::Result,
}
}
fn emit_all(n: &Node, lets: &[(Node, Dom)], code: &mut Vec<Instr>) {
for (k, (l, _)) in lets.iter().enumerate() {
emit(l, &lets[..k], code);
code.push(Instr::Store(k));
}
emit(n, lets, code);
}
fn emit(n: &Node, lets: &[(Node, Dom)], code: &mut Vec<Instr>) {
if let Some(k) = lets.iter().position(|(l, _)| l == n) {
code.push(Instr::Let(k));
return;
}
match n {
Node::Leaf(i) => code.push(Instr::Load(*i)),
Node::Monad(op, y) => {
emit(y, lets, code);
code.push(Instr::Monad(*op));
}
Node::Window(op, k, y) => {
emit(y, lets, code);
code.push(Instr::Window(*op, *k));
}
Node::Scan(op, y) => {
emit(y, lets, code);
code.push(Instr::Scan(*op));
}
Node::Dyad(op, x, y) => {
emit(x, lets, code);
emit(y, lets, code);
code.push(Instr::Dyad(*op));
}
}
}
fn slots(code: &[Instr]) -> usize {
let mut stack: Vec<bool> = Vec::new();
let mut live = 0usize;
let mut max = 1usize;
for ins in code {
let operands = match ins {
Instr::Load(_) => {
stack.push(false);
continue;
}
Instr::Let(_) => {
stack.push(false);
continue;
}
Instr::Store(_) => {
stack.pop();
continue;
}
Instr::Monad(_) | Instr::Window(..) | Instr::Scan(_) => 1,
Instr::Dyad(_) => 2,
};
max = max.max(live + 1);
for _ in 0..operands {
if stack.pop().unwrap_or(false) {
live -= 1;
}
}
live += 1;
stack.push(true);
}
max
}
fn replayable(e: &Expr) -> bool {
match e {
Expr::Const(..) | Expr::Param(..) | Expr::Name(..) => true,
Expr::Assign { .. }
| Expr::PrintPass { .. }
| Expr::Input { .. }
| Expr::Elided { .. }
| Expr::Control(..)
| Expr::AmendIndex { .. }
| Expr::VerbDef { .. }
| Expr::ModDef { .. } => false,
Expr::Monad { verb, y, .. } => verb.is_pure() && replayable(y),
Expr::Dyad { verb, x, y, .. } => verb.is_pure() && replayable(x) && replayable(y),
Expr::Fused { inputs, .. } => inputs.iter().all(replayable),
}
}
fn same(a: &Expr, b: &Expr) -> bool {
match (a, b) {
(Expr::Const(p, _), Expr::Const(q, _)) => p == q,
(Expr::Param(p, _), Expr::Param(q, _)) => p == q,
(Expr::Name(p, _), Expr::Name(q, _)) => p == q,
(Expr::Monad { verb: u, y: p, .. }, Expr::Monad { verb: v, y: q, .. }) => {
same_verb(u, v) && same(p, q)
}
(
Expr::Dyad { verb: u, x: px, y: py, .. },
Expr::Dyad { verb: v, x: qx, y: qy, .. },
) => same_verb(u, v) && same(px, qx) && same(py, qy),
_ => false,
}
}
fn same_verb(a: &Verb, b: &Verb) -> bool {
match (a, b) {
(Verb::Prim(p), Verb::Prim(q)) => p == q,
(Verb::Rank(u, r), Verb::Rank(v, s)) => r == s && same_verb(u, v),
(Verb::Reduce(u), Verb::Reduce(v)) | (Verb::Commute(u), Verb::Commute(v)) => {
same_verb(u, v)
}
(Verb::Windowed(u, j), Verb::Windowed(v, k)) => j == k && same_verb(u, v),
(Verb::PowerN(u, m), Verb::PowerN(v, n)) => m == n && same_verb(u, v),
(Verb::Fork(f, g, h), Verb::Fork(f2, g2, h2)) => {
same_verb(f, f2) && same_verb(g, g2) && same_verb(h, h2)
}
(Verb::NounFork(m, g, h), Verb::NounFork(n, g2, h2)) => {
m == n && same_verb(g, g2) && same_verb(h, h2)
}
(Verb::Hook(g, h), Verb::Hook(g2, h2))
| (Verb::Atop(g, h), Verb::Atop(g2, h2))
| (Verb::Compose(g, h), Verb::Compose(g2, h2)) => same_verb(g, g2) && same_verb(h, h2),
(Verb::BondLeft(m, u), Verb::BondLeft(n, v)) => m == n && same_verb(u, v),
(Verb::BondRight(u, m), Verb::BondRight(v, n)) => m == n && same_verb(u, v),
_ => false,
}
}
pub fn pass(stmts: &mut Vec<Expr>, tol: Tol) {
let orig = std::mem::take(stmts);
let mut cur = orig.clone();
let mut names = 0usize;
let mut crossed = false;
for _ in 0..=orig.len() {
match inline_once(&cur, &mut names, tol) {
Some(next) => {
cur = next;
crossed = true;
}
None => break,
}
}
let mut out: Vec<Expr> = cur.into_iter().map(|e| fuse_expr(e, tol)).collect();
if crossed {
out.insert(0, Expr::Elided { orig, span: Span::new(0, 0) });
}
*stmts = out;
}
fn fuse_expr(e: Expr, tol: Tol) -> Expr {
if let Some(f) = try_fuse(&e, tol) {
return f;
}
match e {
Expr::Assign { name, value, scope, span } => {
Expr::Assign { name, value: Box::new(fuse_expr(*value, tol)), scope, span }
}
Expr::Monad { verb, y, span } => {
Expr::Monad { verb, y: Box::new(fuse_expr(*y, tol)), span }
}
Expr::Dyad { verb, x, y, span } => Expr::Dyad {
verb,
x: Box::new(fuse_expr(*x, tol)),
y: Box::new(fuse_expr(*y, tol)),
span,
},
Expr::PrintPass { value, bare, span } => {
Expr::PrintPass { value: Box::new(fuse_expr(*value, tol)), bare, span }
}
other => other,
}
}
fn build<'a>(
root: &'a Expr,
yields: Yield,
least: usize,
sub: &mut Option<Inline<'a>>,
tol: Tol,
) -> Option<(FusedKernel, Vec<&'a Expr>)> {
if let Some(s) = sub.as_mut() {
s.hits = 0;
}
let plan = plan_of(root, sub.as_ref());
let mut lv = Leaves::default();
let node = chain(root, &mut lv, sub, plan, Dom::Result);
if ops(&node) < least || !lv.inputs.iter().all(|l| replayable(l)) {
return None;
}
let mut code = Vec::new();
let lets = lets_of(&node);
emit_all(&node, &lets, &mut code);
let window = code.iter().find_map(|i| match i {
Instr::Window(_, k) => Some(*k),
_ => None,
});
let scans = code.iter().filter(|i| matches!(i, Instr::Scan(_))).count();
if scans > 0 && matches!(yields, Yield::Reduce(_)) {
return None;
}
let kernel = FusedKernel {
slots: slots(&code),
code,
yields,
leaves: lv.order,
doms: lv.doms,
let_doms: lets.iter().map(|(_, d)| *d).collect(),
plan,
window,
scans,
tol,
};
Some((kernel, lv.inputs))
}
fn kernel_at<'a>(
e: &'a Expr,
sub: &mut Option<Inline<'a>>,
tol: Tol,
) -> Option<(FusedKernel, Vec<&'a Expr>, &'a Expr)> {
if let Expr::Monad { verb, y, .. } = e {
if is_tally(verb) && let Some((k, l)) = build(y, Yield::Tally, 1, sub, tol) {
return Some((k, l, e));
}
if let Some(op) = absorbable_reduce(verb)
&& let Some((k, l)) = build(y, Yield::Reduce(op), 1, sub, tol)
{
return Some((k, l, e));
}
}
let (k, l) = build(e, Yield::Values, 2, sub, tol)?;
Some((k, l, e))
}
fn try_fuse(e: &Expr, tol: Tol) -> Option<Expr> {
let (kernel, leaves, orig) = kernel_at(e, &mut None, tol)?;
let inputs = leaves.into_iter().map(|l| fuse_expr(l.clone(), tol)).collect();
Some(Expr::Fused {
kernel,
inputs,
orig: Box::new(orig.clone()),
span: e.span(),
})
}
pub(crate) fn fallback_tree(k: &FusedKernel, orig: &Expr, values: &[Array]) -> Expr {
let mut next = 0;
let plan = k.plan;
let tree = match orig {
Expr::Monad { verb, y, span } if matches!(k.yields, Yield::Reduce(_)) => Expr::Monad {
verb: verb.clone(),
y: Box::new(substitute(y, values, k, &mut next, plan, Dom::Result)),
span: *span,
},
Expr::Monad { verb, y, .. } if k.yields == Yield::Tally && is_tally(verb) => {
substitute(y, values, k, &mut next, plan, Dom::Result)
}
e => substitute(e, values, k, &mut next, plan, Dom::Result),
};
debug_assert_eq!(next, k.leaves.len(), "the fallback found different leaves");
tree
}
pub(crate) fn fallback_finish(k: &FusedKernel, v: Array) -> Array {
match k.yields {
Yield::Tally => Array::scalar_i64(v.items() as i64),
_ => v,
}
}
fn substitute(
e: &Expr,
values: &[Array],
k: &FusedKernel,
next: &mut usize,
plan: Plan,
dom: Dom,
) -> Expr {
match e {
Expr::Monad { verb, y, span } if fusable_monad(verb).is_some() => Expr::Monad {
verb: verb.clone(),
y: Box::new(substitute(y, values, k, next, plan, dom)),
span: *span,
},
Expr::Monad { verb, y, span } if plan.scan && absorbable_scan(e).is_some() => {
Expr::Monad {
verb: verb.clone(),
y: Box::new(substitute(y, values, k, next, plan, dom)),
span: *span,
}
}
Expr::Dyad { verb, x, y, span } if fusable_dyad(verb).is_some() => {
let ry = substitute(y, values, k, next, plan, dom);
let rx = substitute(x, values, k, next, plan, dom);
Expr::Dyad { verb: verb.clone(), x: Box::new(rx), y: Box::new(ry), span: *span }
}
Expr::Dyad { verb, x, y, span }
if dom == Dom::Result
&& absorbable_window(e).map(|(_, k)| k) == plan.window
&& plan.window.is_some() =>
{
Expr::Dyad {
verb: verb.clone(),
x: x.clone(),
y: Box::new(substitute(y, values, k, next, plan, Dom::Wide)),
span: *span,
}
}
leaf => {
let v = values[k.leaves[*next]].clone();
*next += 1;
Expr::Const(v, leaf.span())
}
}
}
fn hoisted_name(n: &mut usize) -> String {
*n += 1;
format!("·{}", *n - 1)
}
fn inline_once(stmts: &[Expr], names: &mut usize, tol: Tol) -> Option<Vec<Expr>> {
for (i, stmt) in stmts.iter().enumerate() {
let Expr::Assign { name, value, span, .. } = stmt else { continue };
if !inlinable(stmts, i, name, value, tol) {
continue;
}
if let Some(out) = rewrite(stmts, i, name, value, *span, names, tol) {
return Some(out);
}
}
None
}
fn inlinable(stmts: &[Expr], i: usize, name: &str, value: &Expr, tol: Tol) -> bool {
if !replayable(value) || mentions(value, name) {
return false;
}
let mut lv = Leaves::default();
if ops(&chain(value, &mut lv, &mut None, plan_of(value, None), Dom::Result)) < 1 {
return false;
}
let mut guarded = vec![name.to_string()];
free_names(value, &mut guarded);
let later = &stmts[i + 1..];
if later.iter().any(|s| assigns_any(s, &guarded)) {
return false;
}
let mut uses = 0;
for stmt in later {
match uses_land(stmt, name, value, tol) {
Some(n) => uses += n,
None => return false,
}
}
uses > 0
}
fn uses_land(e: &Expr, name: &str, def: &Expr, tol: Tol) -> Option<usize> {
let mut sub = Some(Inline { name, def, hits: 0 });
if let Some((_, leaves, _)) = kernel_at(e, &mut sub, tol) {
let mut n = sub.map_or(0, |s| s.hits);
for l in leaves {
n += uses_land(l, name, def, tol)?;
}
return Some(n);
}
match e {
Expr::Name(n, _) if n == name => None,
Expr::Const(..) | Expr::Param(..) | Expr::Name(..) => Some(0),
Expr::Assign { value, .. } | Expr::PrintPass { value, .. } => uses_land(value, name, def, tol),
Expr::Monad { y, .. } => uses_land(y, name, def, tol),
Expr::Dyad { x, y, .. } => Some(uses_land(x, name, def, tol)? + uses_land(y, name, def, tol)?),
Expr::Fused { .. }
| Expr::Elided { .. }
| Expr::Input { .. }
| Expr::Control(..)
| Expr::AmendIndex { .. }
| Expr::VerbDef { .. }
| Expr::ModDef { .. } => None,
}
}
fn rewrite(
stmts: &[Expr],
i: usize,
name: &str,
value: &Expr,
span: Span,
names: &mut usize,
tol: Tol,
) -> Option<Vec<Expr>> {
let mut lv = Leaves::default();
let plan = plan_of(value, None);
chain(value, &mut lv, &mut None, plan, Dom::Result);
let mut hoists = Vec::new();
let mut bound: Vec<Option<String>> = Vec::new();
for l in &lv.inputs {
if matches!(l, Expr::Const(..) | Expr::Param(..) | Expr::Name(..)) {
bound.push(None);
continue;
}
let n = hoisted_name(names);
hoists.push(Expr::Assign {
name: n.clone(),
value: Box::new((*l).clone()),
scope: Scope::Local,
span: l.span(),
});
bound.push(Some(n));
}
let def = with_leaves(value, &lv, &bound, plan, Dom::Result);
let (kernel, leaves) = build(&def, Yield::Tally, 1, &mut None, tol)?;
let inputs = leaves.into_iter().map(|l| fuse_expr(l.clone(), tol)).collect();
let guard = Expr::Assign {
name: hoisted_name(names),
value: Box::new(Expr::Fused {
kernel,
inputs,
orig: Box::new(def.clone()),
span,
}),
scope: Scope::Local,
span,
};
let mut out = stmts[..i].to_vec();
out.extend(hoists);
out.push(guard);
out.extend(stmts[i + 1..].iter().map(|s| replace_name(s, name, &def)));
Some(out)
}
fn with_leaves(e: &Expr, lv: &Leaves<'_>, bound: &[Option<String>], plan: Plan, dom: Dom) -> Expr {
match e {
Expr::Monad { verb, y, span } if fusable_monad(verb).is_some() => Expr::Monad {
verb: verb.clone(),
y: Box::new(with_leaves(y, lv, bound, plan, dom)),
span: *span,
},
Expr::Monad { verb, y, span } if plan.scan && absorbable_scan(e).is_some() => {
Expr::Monad {
verb: verb.clone(),
y: Box::new(with_leaves(y, lv, bound, plan, dom)),
span: *span,
}
}
Expr::Dyad { verb, x, y, span } if fusable_dyad(verb).is_some() => Expr::Dyad {
verb: verb.clone(),
x: Box::new(with_leaves(x, lv, bound, plan, dom)),
y: Box::new(with_leaves(y, lv, bound, plan, dom)),
span: *span,
},
Expr::Dyad { verb, x, y, span }
if dom == Dom::Result
&& absorbable_window(e).map(|(_, k)| k) == plan.window
&& plan.window.is_some() =>
{
Expr::Dyad {
verb: verb.clone(),
x: x.clone(),
y: Box::new(with_leaves(y, lv, bound, plan, Dom::Wide)),
span: *span,
}
}
leaf => {
let bind = lv
.inputs
.iter()
.position(|&p| same(p, leaf))
.and_then(|i| bound[i].as_ref());
match bind {
Some(n) => Expr::Name(n.clone(), leaf.span()),
None => leaf.clone(),
}
}
}
}
fn replace_name(e: &Expr, name: &str, def: &Expr) -> Expr {
match e {
Expr::Name(n, _) if n == name => def.clone(),
Expr::Assign { name: a, value, scope, span } => Expr::Assign {
scope: *scope,
name: a.clone(),
value: Box::new(replace_name(value, name, def)),
span: *span,
},
Expr::PrintPass { value, bare, span } => Expr::PrintPass {
value: Box::new(replace_name(value, name, def)),
bare: *bare,
span: *span,
},
Expr::Monad { verb, y, span } => Expr::Monad {
verb: verb.clone(),
y: Box::new(replace_name(y, name, def)),
span: *span,
},
Expr::Dyad { verb, x, y, span } => Expr::Dyad {
verb: verb.clone(),
x: Box::new(replace_name(x, name, def)),
y: Box::new(replace_name(y, name, def)),
span: *span,
},
other => other.clone(),
}
}
fn mentions(e: &Expr, name: &str) -> bool {
let mut names = Vec::new();
free_names(e, &mut names);
names.iter().any(|n| n == name)
}
fn free_names(e: &Expr, out: &mut Vec<String>) {
match e {
Expr::Name(n, _) => out.push(n.clone()),
Expr::Assign { value, .. } | Expr::PrintPass { value, .. } => free_names(value, out),
Expr::Monad { y, .. } => free_names(y, out),
Expr::Dyad { x, y, .. } => {
free_names(x, out);
free_names(y, out);
}
Expr::Fused { inputs, .. } => inputs.iter().for_each(|i| free_names(i, out)),
Expr::Const(..)
| Expr::Param(..)
| Expr::Elided { .. }
| Expr::Input { .. }
| Expr::Control(..)
| Expr::AmendIndex { .. }
| Expr::VerbDef { .. }
| Expr::ModDef { .. } => {}
}
}
fn assigns_any(e: &Expr, names: &[String]) -> bool {
match e {
Expr::Assign { name, value, .. } => {
names.iter().any(|n| n == name) || assigns_any(value, names)
}
Expr::PrintPass { value, .. } => assigns_any(value, names),
Expr::Monad { y, .. } => assigns_any(y, names),
Expr::Dyad { x, y, .. } => assigns_any(x, names) || assigns_any(y, names),
Expr::Fused { inputs, .. } => inputs.iter().any(|i| assigns_any(i, names)),
Expr::Const(..)
| Expr::Param(..)
| Expr::Name(..)
| Expr::Elided { .. }
| Expr::Input { .. }
| Expr::Control(..)
| Expr::AmendIndex { .. }
| Expr::VerbDef { .. }
| Expr::ModDef { .. } => false,
}
}
pub fn is_fused(p: &Program) -> bool {
fn any(e: &Expr) -> bool {
match e {
Expr::Fused { .. } => true,
Expr::Const(..)
| Expr::Param(..)
| Expr::Name(..)
| Expr::Elided { .. }
| Expr::Input { .. }
| Expr::Control(..)
| Expr::AmendIndex { .. }
| Expr::VerbDef { .. }
| Expr::ModDef { .. } => false,
Expr::Assign { value, .. } | Expr::PrintPass { value, .. } => any(value),
Expr::Monad { y, .. } => any(y),
Expr::Dyad { x, y, .. } => any(x) || any(y),
}
}
p.stmts.iter().any(any)
}
pub fn is_inlined(p: &Program) -> bool {
matches!(p.stmts.first(), Some(Expr::Elided { .. }))
}
pub fn unfused(p: &Program) -> Program {
fn strip(e: &Expr) -> Expr {
match e {
Expr::Fused { orig, .. } => strip(orig),
Expr::Assign { name, value, scope, span } => {
Expr::Assign {
name: name.clone(),
value: Box::new(strip(value)),
scope: *scope,
span: *span,
}
}
Expr::PrintPass { value, bare, span } => {
Expr::PrintPass { value: Box::new(strip(value)), bare: *bare, span: *span }
}
Expr::Monad { verb, y, span } => {
Expr::Monad { verb: verb.clone(), y: Box::new(strip(y)), span: *span }
}
Expr::Dyad { verb, x, y, span } => Expr::Dyad {
verb: verb.clone(),
x: Box::new(strip(x)),
y: Box::new(strip(y)),
span: *span,
},
other => other.clone(),
}
}
let mut out = p.clone();
let stmts = match p.stmts.first() {
Some(Expr::Elided { orig, .. }) => orig,
_ => &p.stmts,
};
out.stmts = stmts.iter().map(strip).collect();
out
}
fn monad_type(op: ScalarMonad, a: DType) -> Option<DType> {
use DType::*;
use ScalarMonad::*;
if a == Complex {
return None;
}
Some(match op {
Recip | Halve | Exp => F64,
Conj | Abs | OneMinus => a,
Neg | Signum | Inc | Dec | Double | Square => match a {
Bool | I64 => I64,
other => other,
},
Floor | Ceil => match a {
Bool | I64 => I64,
_ => return None,
},
_ => return None,
})
}
fn dyad_type(op: ScalarDyad, a: DType, b: DType) -> Option<DType> {
use ScalarDyad::*;
if a == DType::Complex || b == DType::Complex {
return None;
}
match op {
Eq | Ne | Lt | Le | Gt | Ge => Some(DType::Bool),
DivJ => Some(DType::F64),
Add | Sub | Mul | Min | Max | Residue => match DType::promote(a, b)? {
DType::Bool => Some(DType::I64),
DType::Char | DType::Symbol => None,
t => Some(t),
},
_ => None,
}
}
fn fold_type(op: ScalarDyad, a: DType) -> Option<DType> {
use ScalarDyad::*;
if !matches!(op, Add | Mul | Min | Max) {
return None;
}
match a {
DType::Bool | DType::I64 => Some(DType::I64),
DType::F64 => Some(DType::F64),
_ => None,
}
}
pub(crate) fn working_type(k: &FusedKernel, inputs: &[Array]) -> Option<(DType, DType)> {
let mut stack: Vec<DType> = Vec::with_capacity(k.slots);
let mut lets: Vec<DType> = Vec::new();
let mut float = false;
let mut integer_step = false;
if inputs.iter().any(|a| a.dtype() == DType::Complex || a.dtype().is_exact()) {
return None;
}
for ins in &k.code {
let t = match ins {
Instr::Load(i) => inputs[*i].dtype(),
Instr::Monad(op) => monad_type(*op, stack.pop()?)?,
Instr::Window(op, _) | Instr::Scan(op) => fold_type(*op, stack.pop()?)?,
Instr::Dyad(op) => {
let b = stack.pop()?;
let a = stack.pop()?;
dyad_type(*op, a, b)?
}
Instr::Store(k) => {
let t = stack.pop()?;
if lets.len() != *k {
return None;
}
lets.push(t);
continue;
}
Instr::Let(k) => {
let t = *lets.get(*k)?;
float |= t == DType::F64;
stack.push(t);
continue;
}
};
if !t.is_numeric() {
return None;
}
float |= t == DType::F64;
integer_step |= t == DType::I64 && !matches!(ins, Instr::Load(_));
stack.push(t);
}
let root = stack.pop()?;
let working = if float { DType::F64 } else { DType::I64 };
if working == DType::F64 && integer_step {
return None;
}
Some((working, root))
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum On {
Result,
Wide,
Either,
}
fn combine(a: On, b: On) -> On {
if a == On::Either {
b
} else {
a
}
}
fn placed(d: Option<Dom>) -> On {
match d {
Some(Dom::Result) => On::Result,
Some(Dom::Wide) => On::Wide,
None => On::Either,
}
}
#[derive(Clone, Copy)]
struct Extent {
start: usize,
len: usize,
wide_start: usize,
wide_len: usize,
}
impl Extent {
fn of(start: usize, len: usize, window: Option<usize>, wide: usize) -> Extent {
let Some(k) = window else {
return Extent { start, len, wide_start: start, wide_len: len };
};
let lo = start - start % k;
let hi = ((start + len + k - 2) / k + 1) * k;
Extent { start, len, wide_start: lo, wide_len: hi.min(wide) - lo }
}
fn len_on(&self, dom: On) -> usize {
match dom {
On::Wide => self.wide_len,
_ => self.len,
}
}
}
#[derive(Clone, Copy)]
struct Loaded<'a, T> {
data: &'a [T],
splat: bool,
on: On,
base: usize,
}
impl<T> Loaded<'_, T> {
#[inline]
fn block(&self, at: &Extent, dom: On) -> &[T] {
if self.splat {
return &self.data[..at.len_on(dom)];
}
let (start, len) = read_over(self.on, at);
&self.data[start - self.base..start - self.base + len]
}
}
#[inline]
fn read_over(on: On, at: &Extent) -> (usize, usize) {
match on {
On::Wide => (at.wide_start, at.wide_len),
_ => (at.start, at.len),
}
}
#[derive(Clone, Copy)]
enum Narrow<'a> {
I64(&'a [i64]),
Bool(&'a [u8]),
}
enum Source<'a, T> {
Ready(Loaded<'a, T>),
Staged(Narrow<'a>, On),
}
trait FromNarrow: Copy {
fn fill(src: Narrow<'_>, at: usize, dst: &mut [Self]);
}
impl FromNarrow for f64 {
#[inline]
fn fill(src: Narrow<'_>, at: usize, dst: &mut [f64]) {
match src {
Narrow::I64(v) => {
for (slot, &x) in dst.iter_mut().zip(&v[at..]) {
*slot = x as f64;
}
}
Narrow::Bool(v) => {
for (slot, &x) in dst.iter_mut().zip(&v[at..]) {
*slot = x as f64;
}
}
}
}
}
impl FromNarrow for i64 {
#[inline]
fn fill(src: Narrow<'_>, at: usize, dst: &mut [i64]) {
match src {
Narrow::I64(v) => dst.copy_from_slice(&v[at..at + dst.len()]),
Narrow::Bool(v) => {
for (slot, &x) in dst.iter_mut().zip(&v[at..]) {
*slot = x as i64;
}
}
}
}
}
struct Sources<'a, T> {
of: Vec<Source<'a, T>>,
staged: usize,
}
impl<T: FromNarrow + Default> Sources<'_, T> {
fn with_block<R>(
&self,
at: &Extent,
stage: &mut [T],
width: usize,
f: impl FnOnce(&[Loaded<'_, T>]) -> R,
) -> R {
let mut k = 0;
for s in &self.of {
let Source::Staged(src, on) = s else { continue };
let (start, len) = read_over(*on, at);
T::fill(*src, start, &mut stage[k * width..k * width + len]);
k += 1;
}
let mut k = 0;
let loaded: Vec<Loaded<'_, T>> = self
.of
.iter()
.map(|s| match s {
Source::Ready(l) => *l,
Source::Staged(_, on) => {
let (start, len) = read_over(*on, at);
let d = &stage[k * width..k * width + len];
k += 1;
Loaded { data: d, splat: false, on: *on, base: start }
}
})
.collect();
f(&loaded)
}
}
#[derive(Clone, Copy)]
enum Slot {
Input(usize),
Block(usize, On),
}
struct Scratch<T> {
cells: Vec<T>,
width: usize,
free: Vec<usize>,
stack: Vec<Slot>,
lets: Vec<usize>,
carry: Vec<Option<T>>,
}
impl<T: Copy + Default> Scratch<T> {
fn new(k: &FusedKernel, w: usize) -> Scratch<T> {
let width = w + 3 * k.window.unwrap_or(0);
Scratch {
cells: vec![T::default(); k.slots * width],
width,
free: Vec::with_capacity(k.slots),
stack: Vec::with_capacity(k.slots),
lets: Vec::new(),
carry: vec![None; k.scans],
}
}
}
struct Steps<M, D, W, S> {
monad: M,
dyad: D,
window: W,
scan: S,
}
fn split_slots<'s, T>(
scratch: &'s mut [T],
w: usize,
d: usize,
) -> (&'s mut [T], impl Fn(usize) -> &'s [T]) {
let (lo, hi) = scratch.split_at_mut(d * w);
let (dst, hi) = hi.split_at_mut(w);
let lo: &[T] = lo;
let hi: &[T] = hi;
(dst, move |i: usize| {
if i < d {
&lo[i * w..(i + 1) * w]
} else {
&hi[(i - d - 1) * w..(i - d) * w]
}
})
}
fn exec_block<T, M, D, W, S>(
k: &FusedKernel,
srcs: &[Loaded<'_, T>],
at: &Extent,
sc: &mut Scratch<T>,
out: Option<&mut [T]>,
steps: &Steps<M, D, W, S>,
) -> Option<usize>
where
T: Copy,
M: Fn(ScalarMonad, &[T], &mut [T]) -> bool,
D: Fn(ScalarDyad, &[T], &[T], &mut [T]) -> bool,
W: Fn(ScalarDyad, usize, &[T], usize, &mut [T]) -> bool,
S: Fn(ScalarDyad, &[T], Option<T>, &mut [T]) -> Option<T>,
{
let Scratch { cells, width, free, stack, lets, carry } = sc;
let w = *width;
stack.clear();
free.clear();
lets.clear();
let nslots = cells.len() / w;
free.extend((0..nslots).rev());
let place = |s: &Slot| match s {
Slot::Input(j) => srcs[*j].on,
Slot::Block(_, o) => *o,
};
let last = k.code.len() - 1;
let head = if out.is_some() { last } else { k.code.len() };
let mut scanned = 0usize;
for ins in &k.code[..head] {
match ins {
Instr::Load(j) => stack.push(Slot::Input(*j)),
Instr::Monad(op) => {
let a = stack.pop()?;
let dom = place(&a);
let len = at.len_on(dom);
let d = free.pop()?;
let (dst, get) = split_slots(cells, w, d);
let av = match a {
Slot::Input(j) => srcs[j].block(at, dom),
Slot::Block(i, _) => &get(i)[..len],
};
if !(steps.monad)(*op, av, &mut dst[..len]) {
return None;
}
release(free, lets, a);
stack.push(Slot::Block(d, dom));
}
Instr::Scan(op) => {
let a = stack.pop()?;
let dom = place(&a);
let len = at.len_on(dom);
let d = free.pop()?;
let (dst, get) = split_slots(cells, w, d);
let av = match a {
Slot::Input(j) => srcs[j].block(at, dom),
Slot::Block(i, _) => &get(i)[..len],
};
carry[scanned] = Some((steps.scan)(*op, av, carry[scanned], &mut dst[..len])?);
scanned += 1;
release(free, lets, a);
stack.push(Slot::Block(d, dom));
}
Instr::Window(op, size) => {
let a = stack.pop()?;
let d = free.pop()?;
let (dst, get) = split_slots(cells, w, d);
let av = match a {
Slot::Input(j) => srcs[j].block(at, On::Wide),
Slot::Block(i, _) => &get(i)[..at.wide_len],
};
let first = at.start - at.wide_start;
if !(steps.window)(*op, *size, av, first, &mut dst[..at.len]) {
return None;
}
release(free, lets, a);
stack.push(Slot::Block(d, On::Result));
}
Instr::Dyad(op) => {
let b = stack.pop()?;
let a = stack.pop()?;
let dom = combine(place(&a), place(&b));
let len = at.len_on(dom);
let d = free.pop()?;
let (dst, get) = split_slots(cells, w, d);
let av = match a {
Slot::Input(j) => srcs[j].block(at, dom),
Slot::Block(i, _) => &get(i)[..len],
};
let bv = match b {
Slot::Input(j) => srcs[j].block(at, dom),
Slot::Block(i, _) => &get(i)[..len],
};
if !(steps.dyad)(*op, av, bv, &mut dst[..len]) {
return None;
}
for s in [a, b] {
release(free, lets, s);
}
stack.push(Slot::Block(d, dom));
}
Instr::Store(j) => {
let Slot::Block(i, _) = stack.pop()? else { return None };
if lets.len() != *j {
return None;
}
lets.push(i);
}
Instr::Let(j) => {
stack.push(Slot::Block(*lets.get(*j)?, placed(Some(*k.let_doms.get(*j)?))))
}
}
}
let Some(dst) = out else {
return match stack.pop()? {
Slot::Block(i, _) => Some(i),
Slot::Input(_) => None,
};
};
let dst = &mut dst[..at.len];
let view = |s: Slot, dom: On| match s {
Slot::Input(j) => srcs[j].block(at, dom),
Slot::Block(i, o) => &cells[i * w..i * w + at.len_on(o)],
};
let ok = match k.code[last] {
Instr::Monad(op) => {
let a = stack.pop()?;
(steps.monad)(op, view(a, On::Result), dst)
}
Instr::Scan(op) => {
let a = stack.pop()?;
match (steps.scan)(op, view(a, On::Result), carry[scanned], dst) {
Some(c) => {
carry[scanned] = Some(c);
true
}
None => false,
}
}
Instr::Window(op, size) => {
let a = stack.pop()?;
let first = at.start - at.wide_start;
(steps.window)(op, size, view(a, On::Wide), first, dst)
}
Instr::Dyad(op) => {
let b = stack.pop()?;
let a = stack.pop()?;
let dom = combine(place(&a), place(&b));
(steps.dyad)(op, view(a, dom), view(b, dom), dst)
}
Instr::Load(_) | Instr::Store(_) | Instr::Let(_) => return None,
};
ok.then_some(usize::MAX)
}
fn release(free: &mut Vec<usize>, lets: &[usize], s: Slot) {
if let Slot::Block(i, _) = s
&& !lets.contains(&i)
{
free.push(i);
}
}
fn map_pass<T, M, D, W, S>(
k: &FusedKernel,
srcs: &Sources<'_, T>,
n: usize,
wide: usize,
steps: &Steps<M, D, W, S>,
) -> Option<Vec<T>>
where
T: FromNarrow + Default + Send + Sync,
M: Fn(ScalarMonad, &[T], &mut [T]) -> bool + Sync + Send,
D: Fn(ScalarDyad, &[T], &[T], &mut [T]) -> bool + Sync + Send,
W: Fn(ScalarDyad, usize, &[T], usize, &mut [T]) -> bool + Sync + Send,
S: Fn(ScalarDyad, &[T], Option<T>, &mut [T]) -> Option<T> + Sync + Send,
{
let run = |start: usize, part: &mut [T]| {
let w = BLOCK.min(part.len()).max(1);
let mut sc = Scratch::new(k, w);
let width = sc.width;
let mut stage = vec![T::default(); srcs.staged * width];
for (b, chunk) in part.chunks_mut(w).enumerate() {
let at = Extent::of(start + b * w, chunk.len(), k.window, wide);
let done = srcs.with_block(&at, &mut stage, width, |loaded| {
exec_block(k, loaded, &at, &mut sc, Some(chunk), steps).is_some()
});
if !done {
return false;
}
}
true
};
if k.scans > 0 {
let mut out = vec![T::default(); n];
return run(0, &mut out).then_some(out);
}
let (out, ok) = par::fill(n, run);
ok.then_some(out)
}
const FOLD_LANES: usize = 8;
const MIN_LANE_WORK: usize = 8 * FOLD_LANES;
#[inline(always)]
fn fold_block_body<T, S>(v: &[T], step: &S) -> Option<T>
where
T: Copy,
S: Fn(T, T) -> Option<T>,
{
let n = v.len();
if n < MIN_LANE_WORK {
let mut acc = v[n - 1];
for &x in v[..n - 1].iter().rev() {
acc = step(x, acc)?;
}
return Some(acc);
}
let rows = n / FOLD_LANES;
let head = n - rows * FOLD_LANES;
let last = head + (rows - 1) * FOLD_LANES;
let mut acc = [v[last]; FOLD_LANES];
acc.copy_from_slice(&v[last..last + FOLD_LANES]);
for r in (0..rows - 1).rev() {
let row = &v[head + r * FOLD_LANES..head + (r + 1) * FOLD_LANES];
for (slot, &x) in acc.iter_mut().zip(row) {
*slot = step(x, *slot)?;
}
}
let mut a = acc[FOLD_LANES - 1];
for &x in acc[..FOLD_LANES - 1].iter().rev() {
a = step(x, a)?;
}
for &x in v[..head].iter().rev() {
a = step(x, a)?;
}
Some(a)
}
multiversioned! {
fn fold_block[T: Copy, S: Fn(T, T) -> Option<T>](
v: &[T],
step: &S,
) -> Option<T> = fold_block_body;
}
fn fold_range<T, M, D, W, C, S>(
k: &FusedKernel,
srcs: &Sources<'_, T>,
lo: usize,
hi: usize,
wide: usize,
steps: &Steps<M, D, W, C>,
step: &S,
) -> Option<T>
where
T: FromNarrow + Default,
M: Fn(ScalarMonad, &[T], &mut [T]) -> bool,
D: Fn(ScalarDyad, &[T], &[T], &mut [T]) -> bool,
W: Fn(ScalarDyad, usize, &[T], usize, &mut [T]) -> bool,
C: Fn(ScalarDyad, &[T], Option<T>, &mut [T]) -> Option<T>,
S: Fn(T, T) -> Option<T>,
{
let w = BLOCK.min(hi - lo).max(1);
let mut sc = Scratch::new(k, w);
let width = sc.width;
let mut stage = vec![T::default(); srcs.staged * width];
let mut acc: Option<T> = None;
for b in (0..(hi - lo).div_ceil(w)).rev() {
let start = lo + b * w;
let len = (hi - start).min(w);
let at = Extent::of(start, len, k.window, wide);
let slot = srcs
.with_block(&at, &mut stage, width, |loaded| {
exec_block(k, loaded, &at, &mut sc, None, steps)
})?;
let block = fold_block(&sc.cells[slot * sc.width..slot * sc.width + len], step)?;
acc = Some(match acc {
None => block,
Some(a) => step(block, a)?,
});
}
acc
}
fn reduce_pass<T, M, D, W, C, S>(
k: &FusedKernel,
srcs: &Sources<'_, T>,
n: usize,
wide: usize,
steps: &Steps<M, D, W, C>,
step: S,
) -> Option<T>
where
T: FromNarrow + Default + Send + Sync,
M: Fn(ScalarMonad, &[T], &mut [T]) -> bool + Sync + Send,
D: Fn(ScalarDyad, &[T], &[T], &mut [T]) -> bool + Sync + Send,
W: Fn(ScalarDyad, usize, &[T], usize, &mut [T]) -> bool + Sync + Send,
C: Fn(ScalarDyad, &[T], Option<T>, &mut [T]) -> Option<T> + Sync + Send,
S: Fn(T, T) -> Option<T> + Sync + Send,
{
let chunks = par::chunks(n, n * k.code.len());
if chunks < 2 {
return fold_range(k, srcs, 0, n, wide, steps, &step);
}
let per = n.div_ceil(chunks);
let parts = par::map_indexed(n.div_ceil(per), |c| {
fold_range(k, srcs, c * per, ((c + 1) * per).min(n), wide, steps, &step)
});
let mut it = parts.into_iter().rev();
let mut acc = it.next()??;
for part in it {
acc = step(part?, acc)?;
}
Some(acc)
}
macro_rules! each {
($a:expr, $dst:expr, $f:expr) => {{
let f = $f;
for (slot, &x) in $dst.iter_mut().zip($a) {
*slot = f(x);
}
return true;
}};
}
macro_rules! zip {
($a:expr, $b:expr, $dst:expr, $f:expr) => {{
let f = $f;
for ((slot, &x), &y) in $dst.iter_mut().zip($a).zip($b) {
*slot = f(x, y);
}
return true;
}};
}
#[inline(always)]
fn monad_f64_body(op: ScalarMonad, a: &[f64], dst: &mut [f64], tol: Tol) -> bool {
use ScalarMonad::*;
match op {
Conj => each!(a, dst, |x: f64| x),
Neg => each!(a, dst, |x: f64| -x),
Abs => each!(a, dst, f64::abs),
Signum => each!(a, dst, |x: f64| if tol.is_zero(x) {
0.0
} else if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}),
Recip => each!(a, dst, |x: f64| if x == 0.0 { f64::INFINITY } else { 1.0 / x }),
Floor => each!(a, dst, f64::floor),
Ceil => each!(a, dst, f64::ceil),
Inc => each!(a, dst, |x: f64| x + 1.0),
Dec => each!(a, dst, |x: f64| x - 1.0),
Double => each!(a, dst, |x: f64| x + x),
Halve => each!(a, dst, |x: f64| x / 2.0),
Square => each!(a, dst, |x: f64| x * x),
OneMinus => each!(a, dst, |x: f64| 1.0 - x),
Exp => each!(a, dst, f64::exp),
_ => false,
}
}
#[inline(always)]
fn dyad_f64_body(op: ScalarDyad, a: &[f64], b: &[f64], dst: &mut [f64], tol: Tol) -> bool {
use ScalarDyad::*;
match op {
Add => zip!(a, b, dst, |x: f64, y: f64| x + y),
Sub => zip!(a, b, dst, |x: f64, y: f64| x - y),
Mul => zip!(a, b, dst, |x: f64, y: f64| x * y),
Min => zip!(a, b, dst, f64::min),
Max => zip!(a, b, dst, f64::max),
DivJ => zip!(a, b, dst, |x: f64, y: f64| if y == 0.0 {
if x == 0.0 { 0.0 } else { f64::INFINITY.copysign(x) }
} else {
x / y
}),
Residue => zip!(a, b, dst, |x: f64, y: f64| if x.is_infinite() {
if y == 0.0 || (y > 0.0) == (x > 0.0) { y } else { x }
} else if x == 0.0 {
y
} else {
y - x * (y / x).floor()
}),
Eq | Ne | Lt | Le | Gt | Ge => {
zip!(a, b, dst, |x: f64, y: f64| tol_cmp(op, x, y, tol) as u8 as f64)
}
_ => false,
}
}
macro_rules! each_over {
($a:expr, $dst:expr, $f:expr) => {{
let f = $f;
let mut over = false;
for (slot, &x) in $dst.iter_mut().zip($a) {
let (v, o) = f(x);
*slot = v;
over |= o;
}
return !over;
}};
}
macro_rules! zip_over {
($a:expr, $b:expr, $dst:expr, $f:expr) => {{
let f = $f;
let mut over = false;
for ((slot, &x), &y) in $dst.iter_mut().zip($a).zip($b) {
let (v, o) = f(x, y);
*slot = v;
over |= o;
}
return !over;
}};
}
#[inline(always)]
fn monad_i64_body(op: ScalarMonad, a: &[i64], dst: &mut [i64]) -> bool {
use ScalarMonad::*;
match op {
Conj | Floor | Ceil => each!(a, dst, |x: i64| x),
Neg => each_over!(a, dst, i64::overflowing_neg),
Abs => each_over!(a, dst, i64::overflowing_abs),
Signum => each!(a, dst, i64::signum),
Inc => each_over!(a, dst, |x: i64| x.overflowing_add(1)),
Dec => each_over!(a, dst, |x: i64| x.overflowing_sub(1)),
Double => each_over!(a, dst, |x: i64| x.overflowing_add(x)),
Square => each_over!(a, dst, |x: i64| x.overflowing_mul(x)),
OneMinus => each_over!(a, dst, |x: i64| 1i64.overflowing_sub(x)),
_ => false,
}
}
#[inline(always)]
fn dyad_i64_body(op: ScalarDyad, a: &[i64], b: &[i64], dst: &mut [i64]) -> bool {
use ScalarDyad::*;
match op {
Add => zip_over!(a, b, dst, i64::overflowing_add),
Sub => zip_over!(a, b, dst, i64::overflowing_sub),
Mul => zip_over!(a, b, dst, i64::overflowing_mul),
Min => zip!(a, b, dst, i64::min),
Max => zip!(a, b, dst, i64::max),
Residue => zip!(a, b, dst, |x: i64, y: i64| if x == 0 {
y
} else {
let mut r = y.wrapping_rem(x);
if r != 0 && (r < 0) != (x < 0) {
r += x;
}
r
}),
Eq => zip!(a, b, dst, |x: i64, y: i64| (x == y) as i64),
Ne => zip!(a, b, dst, |x: i64, y: i64| (x != y) as i64),
Lt => zip!(a, b, dst, |x: i64, y: i64| (x < y) as i64),
Le => zip!(a, b, dst, |x: i64, y: i64| (x <= y) as i64),
Gt => zip!(a, b, dst, |x: i64, y: i64| (x > y) as i64),
Ge => zip!(a, b, dst, |x: i64, y: i64| (x >= y) as i64),
_ => false,
}
}
multiversioned! {
fn monad_f64(
op: ScalarMonad,
a: &[f64],
dst: &mut [f64],
tol: Tol,
) -> bool = monad_f64_body;
}
multiversioned! {
fn dyad_f64(
op: ScalarDyad,
a: &[f64],
b: &[f64],
dst: &mut [f64],
tol: Tol,
) -> bool = dyad_f64_body;
}
multiversioned! {
fn monad_i64(op: ScalarMonad, a: &[i64], dst: &mut [i64]) -> bool = monad_i64_body;
}
multiversioned! {
fn dyad_i64(op: ScalarDyad, a: &[i64], b: &[i64], dst: &mut [i64]) -> bool = dyad_i64_body;
}
#[inline(always)]
fn scan_block_body<T, F>(v: &[T], carry: Option<T>, dst: &mut [T], step: &F) -> Option<T>
where
T: Copy,
F: Fn(T, T) -> (T, bool),
{
let mut over = false;
let (mut acc, from) = match carry {
Some(a) => (a, 0),
None => {
dst[0] = v[0];
(v[0], 1)
}
};
for (slot, &x) in dst.iter_mut().zip(v).skip(from) {
let (r, o) = step(acc, x);
acc = r;
over |= o;
*slot = acc;
}
(!over).then_some(acc)
}
multiversioned! {
fn scan_block[T: Copy, F: Fn(T, T) -> (T, bool)](
v: &[T],
carry: Option<T>,
dst: &mut [T],
step: &F,
) -> Option<T> = scan_block_body;
}
fn window_pass_f64(op: ScalarDyad, k: usize, v: &[f64], first: usize, dst: &mut [f64]) -> bool {
use ScalarDyad::*;
match op {
Add => windows_into(v, k, first, dst, &|a: f64, b: f64| (a + b, false)),
Mul => windows_into(v, k, first, dst, &|a: f64, b: f64| (a * b, false)),
Min => windows_into(v, k, first, dst, &|a: f64, b: f64| (a.min(b), false)),
Max => windows_into(v, k, first, dst, &|a: f64, b: f64| (a.max(b), false)),
_ => false,
}
}
fn window_pass_i64(op: ScalarDyad, k: usize, v: &[i64], first: usize, dst: &mut [i64]) -> bool {
use ScalarDyad::*;
match op {
Add => windows_into(v, k, first, dst, &i64::overflowing_add),
Mul => windows_into(v, k, first, dst, &i64::overflowing_mul),
Min => windows_into(v, k, first, dst, &|a: i64, b: i64| (a.min(b), false)),
Max => windows_into(v, k, first, dst, &|a: i64, b: i64| (a.max(b), false)),
_ => false,
}
}
fn scan_pass_f64(op: ScalarDyad, v: &[f64], carry: Option<f64>, dst: &mut [f64]) -> Option<f64> {
use ScalarDyad::*;
match op {
Add => scan_block(v, carry, dst, &|a: f64, b: f64| (a + b, false)),
Mul => scan_block(v, carry, dst, &|a: f64, b: f64| (a * b, false)),
Min => scan_block(v, carry, dst, &|a: f64, b: f64| (a.min(b), false)),
Max => scan_block(v, carry, dst, &|a: f64, b: f64| (a.max(b), false)),
_ => None,
}
}
fn scan_pass_i64(op: ScalarDyad, v: &[i64], carry: Option<i64>, dst: &mut [i64]) -> Option<i64> {
use ScalarDyad::*;
match op {
Add => scan_block(v, carry, dst, &i64::overflowing_add),
Mul => scan_block(v, carry, dst, &i64::overflowing_mul),
Min => scan_block(v, carry, dst, &|a: i64, b: i64| (a.min(b), false)),
Max => scan_block(v, carry, dst, &|a: i64, b: i64| (a.max(b), false)),
_ => None,
}
}
fn step_i64(op: ScalarDyad, a: i64, b: i64) -> Option<i64> {
use ScalarDyad::*;
match op {
Add => a.checked_add(b),
Mul => a.checked_mul(b),
Min => Some(a.min(b)),
Max => Some(a.max(b)),
_ => None,
}
}
pub(crate) fn step(op: ScalarDyad, a: f64, b: f64) -> Option<f64> {
step_f64(op, a, b)
}
fn step_f64(op: ScalarDyad, a: f64, b: f64) -> Option<f64> {
use ScalarDyad::*;
match op {
Add => Some(a + b),
Mul => Some(a * b),
Min => Some(a.min(b)),
Max => Some(a.max(b)),
_ => None,
}
}
fn splat_f64(a: &Array, w: usize) -> Option<Vec<f64>> {
if a.rank() != 0 {
return None;
}
let v = match &a.data {
Data::Bool(d) => d[0] as f64,
Data::I64(d) => d[0] as f64,
Data::F64(d) => d[0],
Data::Ext(_)
| Data::Rat(_)
| Data::Complex(_)
| Data::Char(_)
| Data::Symbol(_)
| Data::Box(_) => {
return Some(Vec::new());
}
};
Some(vec![v; w])
}
fn splat_i64(a: &Array, w: usize) -> Option<Vec<i64>> {
if a.rank() != 0 {
return None;
}
let v = match &a.data {
Data::Bool(d) => d[0] as i64,
Data::I64(d) => d[0],
_ => return Some(Vec::new()),
};
Some(vec![v; w])
}
fn narrow_f64(a: &Array) -> Result<&[f64], Narrow<'_>> {
match &a.data {
Data::I64(d) => Err(Narrow::I64(d)),
Data::Bool(d) => Err(Narrow::Bool(d)),
_ => Ok(a.as_f64_slice().unwrap_or(&[])),
}
}
fn narrow_i64(a: &Array) -> Result<&[i64], Narrow<'_>> {
match &a.data {
Data::Bool(d) => Err(Narrow::Bool(d)),
_ => Ok(a.as_i64_slice().unwrap_or(&[])),
}
}
fn sources<'a, T>(
inputs: &'a [Array],
owned: &'a [Option<Vec<T>>],
on: &impl Fn(usize) -> On,
narrow: impl Fn(&'a Array) -> Result<&'a [T], Narrow<'a>>,
) -> Sources<'a, T> {
let mut staged = 0;
let of = inputs
.iter()
.zip(owned)
.enumerate()
.map(|(j, (a, o))| match o {
Some(v) => Source::Ready(Loaded { data: v, splat: true, on: on(j), base: 0 }),
None => match narrow(a) {
Ok(d) => Source::Ready(Loaded { data: d, splat: false, on: on(j), base: 0 }),
Err(n) => {
staged += 1;
Source::Staged(n, on(j))
}
},
})
.collect();
Sources { of, staged }
}
pub(crate) fn common_shape(inputs: &[Array]) -> Option<Option<Vec<usize>>> {
let mut shape: Option<&Vec<usize>> = None;
for a in inputs {
if a.rank() == 0 {
continue;
}
match shape {
None => shape = Some(&a.shape),
Some(s) if *s == a.shape => {}
Some(_) => return None,
}
}
Some(shape.cloned())
}
struct Axes {
shape: Vec<usize>,
wide: usize,
}
fn axes(k: &FusedKernel, inputs: &[Array]) -> Option<Axes> {
let Some(window) = k.window else {
let shape = common_shape(inputs)??;
if k.scans > 0 && shape.len() != 1 {
return None;
}
return Some(Axes { shape, wide: 0 });
};
let (mut wide, mut result) = (None, None);
for (a, dom) in inputs.iter().zip(&k.doms) {
if a.rank() == 0 {
continue;
}
let (Some(d), 1) = (dom, a.rank()) else { return None };
let seen = if *d == Dom::Wide { &mut wide } else { &mut result };
match seen {
None => *seen = Some(a.shape[0]),
Some(m) if *m == a.shape[0] => {}
Some(_) => return None,
}
}
let wide = wide?;
if wide < window {
return None;
}
let count = wide - window + 1;
if result.is_some_and(|m| m != count) {
return None;
}
Some(Axes { shape: vec![count], wide })
}
pub(crate) fn run(k: &FusedKernel, inputs: &[Array]) -> Option<Array> {
let reducing = matches!(k.yields, Yield::Reduce(_));
let Axes { shape, wide } = axes(k, inputs)?;
let n: usize = shape.iter().product();
if n == 0 {
return None;
}
if reducing && (shape.len() != 1 || n < 2) {
return None;
}
let (working, root) = working_type(k, inputs)?;
if k.yields == Yield::Tally {
return Some(Array::scalar_i64(shape[0] as i64));
}
let w = BLOCK.min(n).max(1) + 3 * k.window.unwrap_or(0);
let tol = k.tol;
let on = |j: usize| placed(k.doms[j]);
let data = if working == DType::F64 {
let steps = Steps {
monad: move |op, a: &[f64], dst: &mut [f64]| monad_f64(op, a, dst, tol),
dyad: move |op, a: &[f64], b: &[f64], dst: &mut [f64]| dyad_f64(op, a, b, dst, tol),
window: window_pass_f64,
scan: scan_pass_f64,
};
let owned: Vec<Option<Vec<f64>>> = inputs.iter().map(|a| splat_f64(a, w)).collect();
let srcs = sources(inputs, &owned, &on, narrow_f64);
match k.reduce() {
None => {
let out = map_pass(k, &srcs, n, wide, &steps)?;
float_result(out, root)
}
Some(op) => {
let v = reduce_pass(k, &srcs, n, wide, &steps, |a, b| step_f64(op, a, b))?;
match root {
DType::F64 => Data::F64(vec![v].into()),
_ => Data::I64(vec![v as i64].into()),
}
}
}
} else {
let steps = Steps {
monad: monad_i64,
dyad: dyad_i64,
window: window_pass_i64,
scan: scan_pass_i64,
};
let owned: Vec<Option<Vec<i64>>> = inputs.iter().map(|a| splat_i64(a, w)).collect();
let srcs = sources(inputs, &owned, &on, narrow_i64);
match k.reduce() {
None => {
let out = map_pass(k, &srcs, n, wide, &steps)?;
int_result(out, root)
}
Some(op) => {
let v = reduce_pass(k, &srcs, n, wide, &steps, |a, b| step_i64(op, a, b))?;
Data::I64(vec![v].into())
}
}
};
Some(Array::new(if reducing { Vec::new() } else { shape }, data))
}
fn float_result(out: Vec<f64>, root: DType) -> Data {
match root {
DType::Bool => Data::Bool(par::map(&out, |&v| (v != 0.0) as u8).into()),
_ => Data::F64(out.into()),
}
}
fn int_result(out: Vec<i64>, root: DType) -> Data {
match root {
DType::Bool => Data::Bool(par::map(&out, |&v| (v != 0) as u8).into()),
_ => Data::I64(out.into()),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Decline {
Agreement,
Empty,
ReduceShape,
WorkingType,
Overflow,
Window,
}
impl Decline {
pub fn reason(self) -> &'static str {
match self {
Decline::Agreement => "the inputs need agreement or are all scalars",
Decline::Empty => "there is nothing to compute",
Decline::ReduceShape => "the reduction needs one axis of two or more items",
Decline::WorkingType => "no single working type holds every step exactly",
Decline::Overflow => "an integer step left 64-bit range",
Decline::Window => "the window does not fit the axis, or the inputs are not aligned with it",
}
}
}
pub fn decline_reason(k: &FusedKernel, inputs: &[Array]) -> Option<Decline> {
let Some(Axes { shape, .. }) = axes(k, inputs) else {
return Some(if k.window.is_some() { Decline::Window } else { Decline::Agreement });
};
let n: usize = shape.iter().product();
if n == 0 {
return Some(Decline::Empty);
}
if matches!(k.yields, Yield::Reduce(_)) && (shape.len() != 1 || n < 2) {
return Some(Decline::ReduceShape);
}
if working_type(k, inputs).is_none() {
return Some(Decline::WorkingType);
}
Some(Decline::Overflow)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Summary {
pub ops: usize,
pub op_names: Vec<String>,
pub reduce: Option<&'static str>,
pub tally: bool,
pub lets: usize,
pub inputs: usize,
pub block: usize,
pub window: Option<usize>,
pub scans: usize,
}
impl std::fmt::Display for Summary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} op{}", self.ops, if self.ops == 1 { "" } else { "s" })?;
if !self.op_names.is_empty() {
write!(f, ": {}", self.op_names.join(" "))?;
}
if let Some(r) = self.reduce {
write!(f, "; {r}/ absorbed")?;
}
if self.tally {
write!(f, "; tally only")?;
}
if self.lets > 0 {
write!(f, "; {} let slot{}", self.lets, if self.lets == 1 { "" } else { "s" })?;
}
if let Some(k) = self.window {
write!(f, "; window {k}")?;
}
if self.scans > 0 {
write!(f, "; {} running fold{}", self.scans, if self.scans == 1 { "" } else { "s" })?;
}
write!(f, "; block {}", self.block)
}
}
pub fn summary(k: &FusedKernel) -> Summary {
let mut op_names: Vec<String> = Vec::new();
let mut lets = 0usize;
for ins in &k.code {
match ins {
Instr::Monad(op) => op_names.push(monad_name(*op).to_string()),
Instr::Dyad(op) => op_names.push(dyad_name(*op).to_string()),
Instr::Window(op, k) => op_names.push(format!("{k} {}/\\", dyad_name(*op))),
Instr::Scan(op) => op_names.push(format!("{}/\\", dyad_name(*op))),
Instr::Store(_) => lets += 1,
Instr::Load(_) | Instr::Let(_) => {}
}
}
Summary {
ops: op_names.len(),
op_names,
reduce: k.reduce().map(dyad_name),
tally: k.yields == Yield::Tally,
lets,
inputs: k.leaves.iter().copied().max().map_or(0, |m| m + 1),
block: BLOCK,
window: k.window,
scans: k.scans,
}
}
pub fn inlined_names(p: &Program) -> Vec<String> {
let Some(Expr::Elided { orig, .. }) = p.stmts.first() else { return Vec::new() };
let assigned = |stmts: &[Expr]| -> Vec<String> {
stmts
.iter()
.filter_map(|s| match s {
Expr::Assign { name, .. } => Some(name.clone()),
_ => None,
})
.collect()
};
let kept = assigned(&p.stmts);
assigned(orig).into_iter().filter(|n| !kept.contains(n)).collect()
}
fn monad_name(op: ScalarMonad) -> &'static str {
use ScalarMonad::*;
match op {
Conj => "+",
Neg => "-",
Signum => "*",
Recip => "%",
Sqrt => "%:",
Exp => "^",
Abs => "|",
Floor => "<.",
Ceil => ">.",
Not => "-.",
OneMinus => "-.",
Inc => ">:",
Dec => "<:",
Double => "+:",
Halve => "-:",
Square => "*:",
Ln => "^.",
Pi => "o.",
Factorial => "!",
Imaginary => "j.",
Polar => "r.",
}
}
fn dyad_name(op: ScalarDyad) -> &'static str {
use ScalarDyad::*;
match op {
Add => "+",
Sub => "-",
Mul => "*",
DivJ | DivApl => "%",
Min => "<.",
Max => ">.",
Pow => "^",
Residue => "|",
Eq => "=",
Ne => "~:",
Lt => "<",
Le => "<:",
Gt => ">",
Ge => ">:",
Lcm => "*.",
Gcd => "+.",
Log => "^.",
Root => "%:",
Circle => "o.",
Binomial => "!",
MakeComplex => "j.",
PolarBy => "r.",
}
}
pub(crate) fn eval_on(
device: Option<&crate::device::Device>,
k: &FusedKernel,
inputs: &[Array],
) -> (Option<Array>, crate::device::Placement) {
use crate::device::Placement;
let mut placement = Placement::Default;
let materialised: Vec<Array>;
let (inputs, layout) = match kernel_layout(inputs) {
Some(l) => (inputs, l),
None => {
materialised = inputs.iter().map(Array::to_row_major).collect();
(&materialised[..], Layout::RowMajor)
}
};
if layout == Layout::RowMajor && let Some(d) = device.filter(|d| d.is_gpu()) {
match crate::device::try_run(d, k, inputs) {
Ok(a) => return (Some(a), Placement::Gpu),
Err(why) => placement = Placement::Cpu(why),
}
}
let r = run(k, inputs).map(|a| a.with_layout(layout));
if r.is_none() {
note_fallback();
}
(r, placement)
}
fn kernel_layout(inputs: &[Array]) -> Option<Layout> {
let mut found: Option<Layout> = None;
for a in inputs {
if a.rank() == 0 {
continue;
}
match found {
None => found = Some(a.layout()),
Some(l) if l == a.layout() => {}
Some(_) => return None,
}
}
Some(found.unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::frontend::{compile, Dialect, Lang};
fn program(src: &str) -> Program {
compile(Lang::J, src, &Dialect::default()).expect("compile")
}
#[test]
fn a_chain_of_two_scalar_verbs_fuses() {
assert!(is_fused(&program("1 + 2 * {x}")));
assert!(is_fused(&program("+/ {w} * {x}")));
assert!(is_fused(&program("+/ ^ {x}")));
}
#[test]
fn one_verb_on_its_own_is_left_alone() {
assert!(!is_fused(&program("2 * {x}")));
assert!(!is_fused(&program("+/ {x}")));
assert!(!is_fused(&program("{x}")));
}
#[test]
fn a_verb_the_kernel_does_not_cover_breaks_the_chain() {
assert!(!is_fused(&program("%: 2 * {x}")));
assert!(is_fused(&program("%: 1 + 2 * {x}")));
}
#[test]
fn an_effect_in_a_leaf_keeps_the_chain_unfused() {
assert!(!is_fused(&program("1 + 2 * echo {x}")));
}
#[test]
fn the_postfix_program_pushes_the_left_operand_first() {
let p = program("{w} - {x} - 1");
let Expr::Fused { kernel, .. } = &p.stmts[0] else { panic!("not fused") };
assert_eq!(
kernel.code(),
[
Instr::Load(2),
Instr::Load(1),
Instr::Load(0),
Instr::Dyad(ScalarDyad::Sub),
Instr::Dyad(ScalarDyad::Sub),
]
);
assert_eq!(kernel.slots, 2);
}
#[test]
fn a_value_the_chain_reads_twice_becomes_a_let() {
let p = program("+/ ({x} + 1) * ({x} + 1)");
let Expr::Fused { kernel, .. } = &p.stmts[0] else { panic!("not fused") };
assert_eq!(
kernel.code(),
[
Instr::Load(1),
Instr::Load(0),
Instr::Dyad(ScalarDyad::Add),
Instr::Store(0),
Instr::Let(0),
Instr::Let(0),
Instr::Dyad(ScalarDyad::Mul),
]
);
assert_eq!(kernel.slots, 2);
}
#[test]
fn a_named_value_moves_into_the_sentence_that_reads_it() {
let p = program("d =. {x} + 1\n+/ d * d");
assert!(is_inlined(&p));
assert_eq!(p.stmts.len(), 3);
let Expr::Fused { kernel, .. } = &p.stmts[2] else { panic!("the sum did not fuse") };
assert!(kernel.code().contains(&Instr::Store(0)));
assert_eq!(unfused(&p).stmts.len(), 2);
}
}