use super::super::{ArrayCompiled, BinCode, UnCode, VarShape};
use super::ir::*;
use ndarray::ArrayD;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::rc::Rc;
use xla::{ArrayElement, ElementType, PrimitiveType, XlaBuilder, XlaComputation, XlaOp};
const RECURRENCE_REFUSAL: &str = "a causal self-reference (recurrence, esm-spec §4.3.1.1) is a \
sequential sweep whose self-read fails closed at run time; this emitter lowers neither \
the ordered per-cell loop nor a channel for that fault, so the model is refused \
(CONFORMANCE_SPEC §5.19.3b)";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct XlaEmitError {
pub rule: String,
pub reason: String,
}
impl XlaEmitError {
pub fn new(rule: impl Into<String>, reason: impl Into<String>) -> Self {
XlaEmitError {
rule: rule.into(),
reason: reason.into(),
}
}
}
impl fmt::Display for XlaEmitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "rule {}: {}", self.rule, self.reason)
}
}
impl std::error::Error for XlaEmitError {}
#[derive(Clone, Copy)]
enum CountCmp {
AxisLeQuery,
AxisLtQuery,
}
fn rm_strides(shape: &[usize]) -> Vec<i64> {
let mut st = vec![0i64; shape.len()];
let mut acc = 1i64;
for d in (0..shape.len()).rev() {
st[d] = acc;
acc *= shape[d] as i64;
}
st
}
pub struct EmittedRhs {
computation: XlaComputation,
n_states: usize,
params_len: usize,
n_instrs: usize,
opcode_counts: Vec<(&'static str, usize)>,
forcing: Option<ForcingFeed>,
}
pub struct ForcingFeed {
buffer: Rc<RefCell<HashMap<String, ArrayD<f64>>>>,
declared: HashSet<String>,
layout: Vec<(ForcingRef, usize)>,
len: usize,
}
impl ForcingFeed {
pub fn len(&self) -> usize {
self.len.max(1)
}
pub fn is_empty(&self) -> bool {
false
}
pub fn pack(&self) -> Vec<f64> {
let mut f = vec![0.0f64; self.len()];
let buffer = self.buffer.borrow();
for (fr, off) in &self.layout {
let n = super::exec::forcing_len(fr);
super::exec::load_forcing(fr, &buffer, &self.declared, false, &mut f[*off..*off + n]);
}
f
}
}
impl EmittedRhs {
pub fn computation(&self) -> &XlaComputation {
&self.computation
}
pub fn n_states(&self) -> usize {
self.n_states
}
pub fn params_len(&self) -> usize {
self.params_len
}
pub fn n_instrs(&self) -> usize {
self.n_instrs
}
pub fn opcode_counts(&self) -> &[(&'static str, usize)] {
&self.opcode_counts
}
pub fn forcing(&self) -> Option<&ForcingFeed> {
self.forcing.as_ref()
}
pub fn take_forcing(&mut self) -> Option<ForcingFeed> {
self.forcing.take()
}
pub fn hlo_text(&self) -> Result<String, XlaEmitError> {
self.computation
.proto()
.to_string()
.map_err(|e| XlaEmitError::new("<program>", format!("HLO dump failed: {e}")))
}
}
pub fn emit_rhs(compiled: &ArrayCompiled) -> Result<EmittedRhs, XlaEmitError> {
let _guard = compiled.precision.enter();
if compiled.precision.is_f32() {
return Err(XlaEmitError::new(
"<program>",
"document working precision is Float32; this emitter lowers the \
Float64 kernel tables only (the tier's precision-changing \
fixtures are excluded, CONFORMANCE_SPEC §5.38.4)",
));
}
if crate::precision::has_variable_overrides() {
return Err(XlaEmitError::new(
"<program>",
"document carries per-variable element types (esm-spec §11.3.1); \
the tape itself is disabled for such documents",
));
}
let (prog, _report) =
compiled.build_tape_layout(&std::collections::HashSet::new(), None, false);
match prog {
Ok(prog) => emit_program(&prog, compiled),
Err(declined) => {
let (rule, reason) = declined
.declined_rules()
.into_iter()
.next()
.expect("a declined tape names at least one rule");
Err(XlaEmitError::new(
rule,
format!(
"rule is not on the tape (the lowering bailed: {reason}); a \
compiled program cannot re-enter the interpreter, so the whole \
model is refused"
),
))
}
}
}
pub(crate) fn emit_program(
prog: &TapeProgram,
compiled: &ArrayCompiled,
) -> Result<EmittedRhs, XlaEmitError> {
debug_assert!(!prog.col_major, "the emitter lowers row-major programs");
let builder = XlaBuilder::new("earthsci_rhs");
let mut em = Emitter::new(&builder, prog, compiled)?;
let du = em.run()?;
let forcing = em.forcing_param.take().map(|_| ForcingFeed {
buffer: Rc::clone(&compiled.forcing),
declared: compiled.declared_names.clone(),
layout: em.forcing_layout.clone(),
len: em.forcing_len,
});
let computation = du
.build()
.map_err(|e| XlaEmitError::new("<program>", format!("XlaBuilder::build failed: {e}")))?;
Ok(EmittedRhs {
computation,
n_states: compiled.n_states,
params_len: prog.params_len.max(1),
n_instrs: prog.instrs.len(),
opcode_counts: opcode_counts(prog),
forcing,
})
}
fn opcode_counts(prog: &TapeProgram) -> Vec<(&'static str, usize)> {
let mut counts: Vec<(&'static str, usize)> = Vec::new();
for i in &prog.instrs {
let op = i.opcode();
match counts.iter_mut().find(|(o, _)| *o == op) {
Some((_, n)) => *n += 1,
None => counts.push((op, 1)),
}
}
counts
}
type R<T> = Result<T, XlaEmitError>;
struct Emitter<'a> {
b: &'a XlaBuilder,
prog: &'a TapeProgram,
u: XlaOp,
p: XlaOp,
t: XlaOp,
slots: Vec<Option<XlaOp>>,
state: Vec<XlaOp>,
blocks: Vec<Option<XlaOp>>,
var_order: Vec<(String, VarShape)>,
var_ix: HashMap<String, usize>,
cur_rule: String,
forcing_param: Option<XlaOp>,
forcing_offsets: Vec<Option<usize>>,
forcing_layout: Vec<(ForcingRef, usize)>,
forcing_len: usize,
}
impl<'a> Emitter<'a> {
fn new(b: &'a XlaBuilder, prog: &'a TapeProgram, compiled: &'a ArrayCompiled) -> R<Self> {
let n_states = compiled.n_states;
let pm = prog.params_len.max(1);
let u = b
.parameter(0, f64::TY, &[n_states as i64], "u")
.map_err(|e| XlaEmitError::new("<program>", format!("parameter u: {e}")))?;
let p = b
.parameter(1, f64::TY, &[pm as i64], "p")
.map_err(|e| XlaEmitError::new("<program>", format!("parameter p: {e}")))?;
let t = b
.parameter(2, f64::TY, &[], "t")
.map_err(|e| XlaEmitError::new("<program>", format!("parameter t: {e}")))?;
let mut var_order: Vec<(String, VarShape)> = compiled
.var_shapes
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
var_order.sort_by_key(|(_, v)| v.flat_offset);
let mut next = 0usize;
for (name, vs) in &var_order {
if vs.flat_offset != next {
return Err(XlaEmitError::new(
"<program>",
format!(
"flat state layout is not contiguous: variable {name} starts at \
{} but {next} was expected",
vs.flat_offset
),
));
}
next += vs.shape.iter().product::<usize>().max(1);
}
if next != n_states {
return Err(XlaEmitError::new(
"<program>",
format!("flat state layout covers {next} slots but the model declares {n_states}"),
));
}
let var_ix: HashMap<String, usize> = var_order
.iter()
.enumerate()
.map(|(i, (n, _))| (n.clone(), i))
.collect();
let mut forcing_offsets: Vec<Option<usize>> = vec![None; prog.forcings.len()];
let mut forcing_layout: Vec<(ForcingRef, usize)> = Vec::new();
let mut forcing_len = 0usize;
for ins in &prog.instrs {
if let Instr::LoadForcing { forcing, .. } = ins {
let ix = *forcing as usize;
if forcing_offsets[ix].is_none() {
let fr = prog.forcings[ix].clone();
forcing_offsets[ix] = Some(forcing_len);
forcing_len += super::exec::forcing_len(&fr);
forcing_layout.push((fr, forcing_offsets[ix].expect("just set")));
}
}
}
let forcing_param = if forcing_layout.is_empty() {
None
} else {
Some(
b.parameter(3, f64::TY, &[forcing_len.max(1) as i64], "f")
.map_err(|e| XlaEmitError::new("<program>", format!("parameter f: {e}")))?,
)
};
let mut em = Emitter {
b,
prog,
u,
p,
t,
slots: vec![None; prog.slots.len()],
state: Vec::new(),
blocks: vec![None; var_order.len()],
var_order,
var_ix,
cur_rule: "<program>".to_string(),
forcing_param,
forcing_offsets,
forcing_layout,
forcing_len,
};
em.state = em.build_state_views()?;
Ok(em)
}
fn build_state_views(&self) -> R<Vec<XlaOp>> {
let mut out = Vec::with_capacity(self.prog.state_vars.len());
for sv in &self.prog.state_vars {
let off = sv.flat_offset as i64;
let n: usize = sv.shape.iter().product::<usize>().max(1);
let flat = self
.u
.slice_in_dim(off, off + n as i64, 1, 0)
.map_err(|e| self.err(format!("state slice for {}: {e}", sv.name)))?;
let op = if sv.shape.is_empty() {
flat.reshape(&[])
.map_err(|e| self.err(format!("state reshape for {}: {e}", sv.name)))?
} else {
let rev: Vec<i64> = sv.shape.iter().rev().map(|&d| d as i64).collect();
let perm: Vec<i64> = (0..sv.shape.len() as i64).rev().collect();
flat.reshape(&rev)
.and_then(|a| a.transpose(&perm))
.map_err(|e| self.err(format!("state relabel for {}: {e}", sv.name)))?
};
out.push(op);
}
Ok(out)
}
fn err(&self, reason: impl Into<String>) -> XlaEmitError {
XlaEmitError::new(self.cur_rule.clone(), reason)
}
fn run(&mut self) -> R<XlaOp> {
self.emit_range(0, self.prog.instrs.len())?;
self.assemble_du()
}
fn dims(&self, op: &XlaOp) -> R<Vec<usize>> {
op.dims().map_err(|e| self.err(format!("dims: {e}")))
}
fn c(&self, v: f64) -> R<XlaOp> {
self.b.c0(v).map_err(|e| self.err(format!("constant: {e}")))
}
fn ci(&self, v: i64) -> R<XlaOp> {
self.b
.c0(v)
.map_err(|e| self.err(format!("s64 constant: {e}")))
}
fn zeros(&self, dims: &[usize]) -> R<XlaOp> {
let z = self.c(0.0)?;
if dims.is_empty() {
return Ok(z);
}
let d: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
z.broadcast(&d).map_err(|e| self.err(format!("zeros: {e}")))
}
fn splat_like(&self, like: &XlaOp, v: f64) -> R<XlaOp> {
let d = self.dims(like)?;
let c = like
.builder()
.c0(v)
.map_err(|e| self.err(format!("constant: {e}")))?;
if d.is_empty() {
return Ok(c);
}
let d: Vec<i64> = d.iter().map(|&x| x as i64).collect();
c.broadcast(&d).map_err(|e| self.err(format!("splat: {e}")))
}
fn to_shape(&self, op: &XlaOp, dims: &[usize]) -> R<XlaOp> {
let have = self.dims(op)?;
if have == dims {
return Ok(op.clone());
}
if have.is_empty() {
let d: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
return op
.broadcast(&d)
.map_err(|e| self.err(format!("broadcast to {dims:?}: {e}")));
}
Err(self.err(format!(
"operand shape {have:?} does not match the instruction's box {dims:?}"
)))
}
fn pred_to_f64(&self, op: XlaOp) -> R<XlaOp> {
op.convert(PrimitiveType::F64)
.map_err(|e| self.err(format!("pred -> f64: {e}")))
}
fn wrap(&self, r: Result<XlaOp, xla::Error>, what: &str) -> R<XlaOp> {
r.map_err(|e| self.err(format!("{what}: {e}")))
}
fn operand(&self, op: &Operand) -> R<XlaOp> {
match op {
Operand::Lit(v) => self.c(*v),
Operand::Time => Ok(self.t.clone()),
Operand::Param(ix) => {
let i = *ix as i64;
self.wrap(
self.p
.slice_in_dim(i, i + 1, 1, 0)
.and_then(|s| s.reshape(&[])),
&format!("parameter {ix}"),
)
}
Operand::Slot(s) => self.slots[*s as usize]
.clone()
.ok_or_else(|| self.err(format!("read of slot {s} before its definition"))),
Operand::State(ix) => Ok(self.state[*ix as usize].clone()),
}
}
fn src(&self, s: &SrcRef) -> R<XlaOp> {
match s {
SrcRef::Slot(id) => self.slots[*id as usize]
.clone()
.ok_or_else(|| self.err(format!("gather source slot {id} is undefined"))),
SrcRef::State(ix) => Ok(self.state[*ix as usize].clone()),
}
}
fn out_dims(&self, slot: SlotId) -> Vec<usize> {
let d = &self.prog.slots[slot as usize];
if d.scalar {
Vec::new()
} else {
d.shape.to_vec()
}
}
fn bin(&self, op: BinCode, a: &XlaOp, b: &XlaOp) -> R<XlaOp> {
let cmp = |r: Result<XlaOp, xla::Error>, what: &str| -> R<XlaOp> {
self.pred_to_f64(self.wrap(r, what)?)
};
match op {
BinCode::Add => self.wrap(a.add_(b), "add"),
BinCode::Sub => self.wrap(a.sub_(b), "sub"),
BinCode::Mul => self.wrap(a.mul_(b), "mul"),
BinCode::Div => self.wrap(a.div_(b), "div"),
BinCode::Pow => self.wrap(a.pow(b), "pow"),
BinCode::Atan2 => self.wrap(a.atan2(b), "atan2"),
BinCode::Min => self.nan_aware(a, b, true),
BinCode::Max => self.nan_aware(a, b, false),
BinCode::Eq => cmp(a.eq(b), "eq"),
BinCode::Ne => cmp(a.ne(b), "ne"),
BinCode::Lt => cmp(a.lt(b), "lt"),
BinCode::Le => cmp(a.le(b), "le"),
BinCode::Gt => cmp(a.gt(b), "gt"),
BinCode::Ge => cmp(a.ge(b), "ge"),
BinCode::And => {
let za = self.splat_like(a, 0.0)?;
let x = self.wrap(a.ne(&za), "and: a != 0")?;
let y = self.wrap(b.ne(&za), "and: b != 0")?;
self.pred_to_f64(self.wrap(x.and(&y), "and")?)
}
BinCode::Or => {
let za = self.splat_like(a, 0.0)?;
let x = self.wrap(a.ne(&za), "or: a != 0")?;
let y = self.wrap(b.ne(&za), "or: b != 0")?;
self.pred_to_f64(self.wrap(x.or(&y), "or")?)
}
BinCode::Unknown => Err(self.err(
"binary operator resolved to BinCode::Unknown (the interpreter's NaN \
sentinel); refusing rather than emitting a silent NaN",
)),
}
}
fn nan_aware(&self, a: &XlaOp, b: &XlaOp, is_min: bool) -> R<XlaOp> {
let base = if is_min {
self.wrap(a.min(b), "min")?
} else {
self.wrap(a.max(b), "max")?
};
let a_nan = self.wrap(a.ne(a), "a is NaN")?;
let b_nan = self.wrap(b.ne(b), "b is NaN")?;
let r = self.wrap(b_nan.select(a, &base), "min/max: b NaN")?;
self.wrap(a_nan.select(b, &r), "min/max: a NaN")
}
fn un(&self, op: UnCode, a: &XlaOp) -> R<XlaOp> {
let one = self.splat_like(a, 1.0)?;
let two = self.splat_like(a, 2.0)?;
let half = self.splat_like(a, 0.5)?;
let zero = self.splat_like(a, 0.0)?;
match op {
UnCode::Exp => self.wrap(a.exp(), "exp"),
UnCode::Ln => self.wrap(a.log(), "log"),
UnCode::Log10 => {
let l = self.wrap(a.log(), "log10: log")?;
let c = self.splat_like(a, std::f64::consts::LN_10)?;
self.wrap(l.div_(&c), "log10: div")
}
UnCode::Sqrt => self.wrap(a.sqrt(), "sqrt"),
UnCode::Abs => self.wrap(a.abs(), "abs"),
UnCode::Sign => {
let neg = self.splat_like(a, -1.0)?;
let pos = self.wrap(a.gt(&zero), "sign: >0")?;
let negp = self.wrap(a.lt(&zero), "sign: <0")?;
let inner = self.wrap(negp.select(&neg, &zero), "sign: inner")?;
self.wrap(pos.select(&one, &inner), "sign: outer")
}
UnCode::Floor => self.wrap(a.floor(), "floor"),
UnCode::Ceil => self.wrap(a.ceil(), "ceil"),
UnCode::Sin => self.wrap(a.sin(), "sin"),
UnCode::Cos => self.wrap(a.cos(), "cos"),
UnCode::Tan => {
let s = self.wrap(a.sin(), "tan: sin")?;
let c = self.wrap(a.cos(), "tan: cos")?;
self.wrap(s.div_(&c), "tan: div")
}
UnCode::Asin => {
let x2 = self.wrap(a.mul_(a), "asin: x^2")?;
let r = self.wrap(one.sub_(&x2), "asin: 1-x^2")?;
let s = self.wrap(r.sqrt(), "asin: sqrt")?;
self.wrap(a.atan2(&s), "asin: atan2")
}
UnCode::Acos => {
let x2 = self.wrap(a.mul_(a), "acos: x^2")?;
let r = self.wrap(one.sub_(&x2), "acos: 1-x^2")?;
let s = self.wrap(r.sqrt(), "acos: sqrt")?;
self.wrap(s.atan2(a), "acos: atan2")
}
UnCode::Atan => self.wrap(a.atan2(&one), "atan"),
UnCode::Sinh => {
let e = self.wrap(a.expm1(), "sinh: expm1")?;
let d = self.wrap(e.add_(&one), "sinh: expm1+1")?;
let q = self.wrap(e.div_(&d), "sinh: ratio")?;
let small = self.wrap(
e.add_(&q).and_then(|s| s.mul_(&half)),
"sinh: small-argument form",
)?;
let ex = self.wrap(a.exp(), "sinh: exp")?;
let en = self.wrap(a.neg().and_then(|n| n.exp()), "sinh: exp(-x)")?;
let big =
self.wrap(ex.sub_(&en).and_then(|s| s.mul_(&half)), "sinh: plain form")?;
let thresh = self.splat_like(a, 0.5)?;
let absx = self.wrap(a.abs(), "sinh: |x|")?;
let use_big = self.wrap(absx.gt(&thresh), "sinh: select")?;
self.wrap(use_big.select(&big, &small), "sinh: merge")
}
UnCode::Cosh => {
let ex = self.wrap(a.exp(), "cosh: exp")?;
let en = self.wrap(a.neg().and_then(|n| n.exp()), "cosh: exp(-x)")?;
self.wrap(ex.add_(&en).and_then(|s| s.mul_(&half)), "cosh")
}
UnCode::Tanh => self.wrap(a.tanh(), "tanh"),
UnCode::Asinh => {
let absx = self.wrap(a.abs(), "asinh: |x|")?;
let x2 = self.wrap(absx.mul_(&absx), "asinh: x^2")?;
let root = self.wrap(x2.add_(&one).and_then(|s| s.sqrt()), "asinh: sqrt")?;
let den = self.wrap(one.add_(&root), "asinh: 1+sqrt")?;
let frac = self.wrap(x2.div_(&den), "asinh: ratio")?;
let small = self.wrap(
absx.add_(&frac).and_then(|s| s.log1p()),
"asinh: small-argument form",
)?;
let big = self.wrap(absx.add_(&root).and_then(|s| s.log()), "asinh: plain form")?;
let use_big = self.wrap(absx.gt(&two), "asinh: select")?;
let mag = self.wrap(use_big.select(&big, &small), "asinh: merge")?;
let negm = self.wrap(mag.neg(), "asinh: negate")?;
let isneg = self.wrap(a.lt(&zero), "asinh: sign")?;
self.wrap(isneg.select(&negm, &mag), "asinh: apply sign")
}
UnCode::Acosh => {
let s = self.wrap(a.sub_(&one), "acosh: x-1")?;
let sp2 = self.wrap(s.add_(&two), "acosh: x+1")?;
let root = self.wrap(s.mul_(&sp2).and_then(|q| q.sqrt()), "acosh: sqrt")?;
let small = self.wrap(
s.add_(&root).and_then(|q| q.log1p()),
"acosh: small-argument form",
)?;
let x2 = self.wrap(a.mul_(a), "acosh: x^2")?;
let root2 = self.wrap(x2.sub_(&one).and_then(|q| q.sqrt()), "acosh: sqrt2")?;
let big = self.wrap(a.add_(&root2).and_then(|q| q.log()), "acosh: plain form")?;
let use_big = self.wrap(a.gt(&two), "acosh: select")?;
self.wrap(use_big.select(&big, &small), "acosh: merge")
}
UnCode::Atanh => {
let num = self.wrap(two.mul_(a), "atanh: 2x")?;
let den = self.wrap(one.sub_(a), "atanh: 1-x")?;
let q = self.wrap(num.div_(&den), "atanh: ratio")?;
self.wrap(q.log1p().and_then(|l| l.mul_(&half)), "atanh: 0.5*log1p")
}
UnCode::Not => {
let p = self.wrap(a.eq(&zero), "not")?;
self.pred_to_f64(p)
}
UnCode::Unknown => Err(self.err(
"unary operator resolved to UnCode::Unknown (the interpreter's NaN \
sentinel); refusing rather than emitting a silent NaN",
)),
}
}
fn emit_range(&mut self, start: usize, end: usize) -> R<()> {
let mut pc = start;
while pc < end {
if let Some(ord) = self.prog.provenance.get(pc) {
if let Some(info) = self.prog.rules.get(*ord as usize) {
self.cur_rule = info.name.clone();
}
}
if let Instr::JmpIfZero {
cond,
n_true,
n_false,
} = &self.prog.instrs[pc]
{
let (nt, nf) = (*n_true as usize, *n_false as usize);
let cond = *cond;
let t_start = pc + 1;
let f_start = t_start + nt;
let f_end = f_start + nf;
if f_end > end {
return Err(self.err(format!(
"JmpIfZero at {pc} spans past the end of its section ({f_end} > {end})"
)));
}
self.emit_branch(&cond, t_start, f_start, f_end)?;
pc = f_end;
continue;
}
if let Instr::Sweep { .. } = &self.prog.instrs[pc] {
return Err(self.err(RECURRENCE_REFUSAL));
}
self.emit_one(pc)?;
pc += 1;
}
Ok(())
}
fn emit_branch(&mut self, cond: &Operand, t0: usize, f0: usize, f1: usize) -> R<()> {
for pc in t0..f1 {
match &self.prog.instrs[pc] {
Instr::DyWrite { .. } | Instr::Export { .. } => {
return Err(self.err(format!(
"instruction {} inside a JmpIfZero branch has an effect beyond \
its slot; this emitter lowers branches as `select` over both \
arms and cannot order such an effect",
self.prog.instrs[pc].opcode()
)));
}
_ => {}
}
}
let c = self.operand(cond)?;
if !self.dims(&c)?.is_empty() {
return Err(self.err("JmpIfZero condition is not a scalar"));
}
let cz = self.c(0.0)?;
let pred0 = self.wrap(c.ne(&cz), "JmpIfZero: cond != 0")?;
let before = self.slots.clone();
let blocks_before = self.blocks.clone();
self.emit_range(t0, f0)?;
let after_t = std::mem::replace(&mut self.slots, before.clone());
let blocks_t = std::mem::replace(&mut self.blocks, blocks_before.clone());
self.emit_range(f0, f1)?;
let after_f = std::mem::take(&mut self.slots);
let blocks_f = std::mem::take(&mut self.blocks);
if blocks_t.len() != blocks_f.len() {
return Err(self.err("JmpIfZero branches disagree on the dy blocks"));
}
self.blocks = blocks_before;
let mut merged = Vec::with_capacity(after_t.len());
for i in 0..after_t.len() {
let v = match (&after_t[i], &after_f[i]) {
(Some(tv), Some(fv)) => {
if before[i].is_some() && std::ptr::eq(tv, fv) {
Some(tv.clone())
} else {
let dims = self.dims(tv)?;
let fd = self.dims(fv)?;
if dims != fd {
return Err(self.err(format!(
"JmpIfZero phi slot {i} has shape {dims:?} on one arm and \
{fd:?} on the other"
)));
}
let pred = if dims.is_empty() {
pred0.clone()
} else {
let d: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
self.wrap(pred0.broadcast(&d), "JmpIfZero: broadcast cond")?
};
Some(self.wrap(pred.select(tv, fv), "JmpIfZero: select")?)
}
}
(Some(v), None) | (None, Some(v)) => Some(v.clone()),
(None, None) => None,
};
merged.push(v);
}
self.slots = merged;
Ok(())
}
fn emit_one(&mut self, pc: usize) -> R<()> {
let instr = self.prog.instrs[pc].clone();
match &instr {
Instr::Bin { op, a, b, out } => {
let dims = self.out_dims(*out);
let av = self.operand(a)?;
let bv = self.operand(b)?;
let av = self.to_shape(&av, &dims)?;
let bv = self.to_shape(&bv, &dims)?;
let v = self.bin(*op, &av, &bv)?;
self.define(*out, v);
}
Instr::Un { op, a, out } => {
let dims = self.out_dims(*out);
let av = self.operand(a)?;
let av = self.to_shape(&av, &dims)?;
let v = self.un(*op, &av)?;
self.define(*out, v);
}
Instr::Neg { a, out } => {
let dims = self.out_dims(*out);
let av = self.operand(a)?;
let av = self.to_shape(&av, &dims)?;
let v = self.wrap(av.neg(), "neg")?;
self.define(*out, v);
}
Instr::Select { cond, a, b, out } => {
let dims = self.out_dims(*out);
let cv = self.operand(cond)?;
let av = self.operand(a)?;
let bv = self.operand(b)?;
let cv = self.to_shape(&cv, &dims)?;
let av = self.to_shape(&av, &dims)?;
let bv = self.to_shape(&bv, &dims)?;
let z = self.splat_like(&cv, 0.0)?;
let pred = self.wrap(cv.ne(&z), "select: cond != 0")?;
let v = self.wrap(pred.select(&av, &bv), "select")?;
self.define(*out, v);
}
Instr::Gather { src, plan, out } => {
let s = self.src(src)?;
let plan = &self.prog.plans[*plan as usize];
let have = self.dims(&s)?;
if have != plan.src_shape.as_slice() {
return Err(self.err(format!(
"gather source has shape {have:?} but the plan expects {:?}",
&plan.src_shape[..]
)));
}
let v = self.emit_gather(plan, &s)?;
self.define(*out, v);
}
Instr::LoadElem { src, idx, out } => {
let s = self.src(src)?;
let mut cur = s;
for (d, &i) in idx.iter().enumerate() {
cur = self.wrap(
cur.slice_in_dim(i as i64, i as i64 + 1, 1, d as i64),
"load_elem: slice",
)?;
}
let v = self.wrap(cur.reshape(&[]), "load_elem: reshape")?;
self.define(*out, v);
}
Instr::Ramp { axis, lo, out } => {
let dims = self.out_dims(*out);
if dims.is_empty() {
return Err(self.err("Ramp into a scalar slot"));
}
let d: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
let io = self.wrap(
self.b.iota(ElementType::S64, &d, *axis as i64),
"ramp: iota",
)?;
let loc = self.ci(*lo)?;
let loc = self.wrap(loc.broadcast(&d), "ramp: broadcast lo")?;
let v = self.wrap(
io.add_(&loc).and_then(|s| s.convert(PrimitiveType::F64)),
"ramp: add + convert",
)?;
self.define(*out, v);
}
Instr::Fill { v, out } => {
let dims = self.out_dims(*out);
let vv = self.operand(v)?;
if !self.dims(&vv)?.is_empty() {
return Err(self.err("Fill with an array operand"));
}
let vv = self.to_shape(&vv, &dims)?;
self.define(*out, vv);
}
Instr::Copy { a, out } => {
let dims = self.out_dims(*out);
let av = self.operand(a)?;
let av = self.to_shape(&av, &dims)?;
self.define(*out, av);
}
Instr::Region {
base,
src,
region,
out,
} => {
let spec = &self.prog.regions[*region as usize];
let basev = self.slots[*base as usize]
.clone()
.ok_or_else(|| self.err("Region base slot is undefined"))?;
let sv = self.operand(src)?;
let upd = self.to_shape(&sv, &spec.shape)?;
let starts: Vec<XlaOp> = spec
.dest_lo
.iter()
.map(|&x| self.ci(x as i64))
.collect::<R<Vec<_>>>()?;
let v = self.wrap(
basev.dynamic_update_slice(&upd, &starts),
"region: dynamic_update_slice",
)?;
self.define(*out, v);
}
Instr::Assemble { table, out } => {
let dims = self.out_dims(*out);
let mut cur = self.zeros(&dims)?;
for (src, region) in &self.prog.assemblies[*table as usize].parts {
let spec = &self.prog.regions[*region as usize];
let sv = self.operand(src)?;
let upd = self.to_shape(&sv, &spec.shape)?;
let starts: Vec<XlaOp> = spec
.dest_lo
.iter()
.map(|&x| self.ci(x as i64))
.collect::<R<Vec<_>>>()?;
cur = self.wrap(
cur.dynamic_update_slice(&upd, &starts),
"assemble: dynamic_update_slice",
)?;
}
self.define(*out, cur);
}
Instr::Export { .. } => {}
Instr::DyWrite { write } => {
let w = self.prog.dy_writes[*write as usize].clone();
self.emit_dy_write(&w)?;
}
Instr::ConstArray { data, out } => {
let payload = &self.prog.const_data[*data as usize];
let dims = self.out_dims(*out);
if payload.shape.as_slice() != dims.as_slice() {
return Err(self.err(format!(
"ConstArray literal has shape {:?} but its slot's box is {dims:?}",
&payload.shape[..]
)));
}
let flat = self.wrap(
self.b.constant_r1(&payload.values[..]),
"const array: literal",
)?;
let v = if dims.is_empty() {
self.wrap(flat.reshape(&[]), "const array: reshape to scalar")?
} else {
let d: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
self.wrap(flat.reshape(&d), "const array: reshape")?
};
self.define(*out, v);
}
Instr::LoadForcing { forcing, out } => {
let fr = &self.prog.forcings[*forcing as usize];
let off = self.forcing_offsets[*forcing as usize]
.ok_or_else(|| self.err("forcing load with no place in `f`"))?;
let n = super::exec::forcing_len(fr);
let f = self
.forcing_param
.clone()
.ok_or_else(|| self.err("forcing load in a program without `f`"))?;
let flat = self.wrap(
f.slice_in_dim(off as i64, (off + n) as i64, 1, 0),
"forcing: slice",
)?;
let dims = self.out_dims(*out);
let d: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
let v = self.wrap(flat.reshape(&d), "forcing: reshape")?;
self.define(*out, v);
}
Instr::Reduce {
op,
init,
src,
axes,
reverse: _,
src_shape,
out,
} => {
let s = self.src(src)?;
let have = self.dims(&s)?;
if have != src_shape.as_slice() {
return Err(self.err(format!(
"reduce source has shape {have:?} but the instruction expects {:?}",
&src_shape[..]
)));
}
let comp = self.reduce_computation(*op)?;
let init_op = self.c(*init)?;
let dims: Vec<i64> = axes.iter().map(|&a| a as i64).collect();
let v = self.wrap(s.reduce(init_op, comp, &dims, false), "reduce")?;
let got = self.dims(&v)?;
let want = self.out_dims(*out);
if got != want {
return Err(self.err(format!(
"reduce produced shape {got:?} but its slot's box is {want:?}"
)));
}
self.define(*out, v);
}
Instr::Scan {
op,
init,
src,
axis,
inclusive,
reverse,
src_shape,
out,
} => {
let s = self.src(src)?;
let have = self.dims(&s)?;
if have != src_shape.as_slice() {
return Err(self.err(format!(
"scan source has shape {have:?} but the instruction expects {:?}",
&src_shape[..]
)));
}
let v = self.emit_scan_loop(
*op,
*init,
&s,
&have,
*axis as usize,
*inclusive,
*reverse,
)?;
self.define(*out, v);
}
Instr::TableGather { src, table, out } => {
let s = self.src(src)?;
let tbl = &self.prog.gather_tables[*table as usize];
let have = self.dims(&s)?;
if have != tbl.src_shape.as_slice() {
return Err(self.err(format!(
"table gather source has shape {have:?} but its table expects {:?}",
&tbl.src_shape[..]
)));
}
let v = self.emit_table_gather(&s, &have, &tbl.pos)?;
self.define(*out, v);
}
Instr::SegReduce {
op,
init,
src,
mask,
table,
out,
} => {
let terms = self.operand(&Operand::Slot(*src))?;
let mask = match mask {
Some(m) => Some(self.operand(&Operand::Slot(*m))?),
None => None,
};
let rows = &self.prog.seg_tables[*table as usize].rows;
let v = self.emit_seg_reduce(*op, *init, &terms, mask.as_ref(), rows)?;
let want = self.out_dims(*out);
let d: Vec<i64> = want.iter().map(|&x| x as i64).collect();
let v = self.wrap(v.reshape(&d), "segmented reduce: output box")?;
self.define(*out, v);
}
Instr::Calendar { func, .. } => {
return Err(self.err(format!(
"`{}` in a Float32 document has no XLA lowering (the calendar is \
evaluated by the closed-function registry in binary64)",
CALENDAR_FNS[*func as usize]
)));
}
Instr::Interp { table, x, y, out } => {
let tbl = &self.prog.interp_tables[*table as usize];
if let Some(id) = &tbl.strict {
return Err(self.err(format!(
"a lookup into table `{id}`, which declares `out_of_bounds: \"error\"` \
(esm-spec §9.5.1): a compiled program cannot raise \
`table_lookup_out_of_bounds` at run time"
)));
}
let dims = self.out_dims(*out);
let xv = self.operand(x)?;
let xv = self.to_shape(&xv, &dims)?;
let yv = match y {
Some(y) => {
let v = self.operand(y)?;
Some(self.to_shape(&v, &dims)?)
}
None => None,
};
let v = self.emit_interp(tbl, &dims, &xv, yv.as_ref())?;
self.define(*out, v);
}
Instr::PolyArea { a, b, geom, out } => {
let v = self.emit_poly_area(a, b, *geom, *out)?;
self.define(*out, v);
}
Instr::IndexGather {
src,
idx,
spec,
out,
} => {
let v = self.emit_index_gather(src, idx, *spec)?;
self.define(*out, v);
}
Instr::Reshape { src, out } => {
let s = self.src(src)?;
let dims = self.out_dims(*out);
let have = self.dims(&s)?;
if have.iter().product::<usize>() != dims.iter().product::<usize>() {
return Err(self.err(format!(
"reshape source has shape {have:?}, which does not hold the \
{dims:?} its slot's box needs"
)));
}
let d: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
let v = self.wrap(s.reshape(&d), "reshape")?;
self.define(*out, v);
}
Instr::Fault { fault } => {
return Err(self.err(format!(
"the program raises a fail-closed evaluation fault, which a compiled \
program cannot report: {}",
self.prog.faults[*fault as usize]
)));
}
Instr::FaultScan { .. } => {
return Err(self.err(
"the program may raise a fail-closed evaluation fault (a read the \
interpreter evaluates only where a condition holds), which a compiled \
program cannot report",
));
}
Instr::JmpIfZero { .. } => {
return Err(self.err("JmpIfZero reached emit_one (handled by emit_range)"));
}
Instr::Sweep { .. } => return Err(self.err(RECURRENCE_REFUSAL)),
Instr::DataGather { faults, .. } => {
return Err(self.err(if *faults {
"a computed-subscript const-array gather (`DataGather`) can raise a \
fail-closed fault, which a compiled program cannot report"
} else {
"a computed-subscript gather (`DataGather`) has no XLA lowering yet"
}));
}
Instr::ScalarRead { .. } => {
return Err(self.err(
"a run-time-subscript read (`ScalarRead`) can raise a fail-closed \
fault, which a compiled program cannot report",
));
}
Instr::Fused { .. } => {
return Err(self.err(
"Instr::Fused reached the emitter; the tape must be built with the \
fusion pass OFF for emission (build_tape_opts(.., None)) — a fused \
group is a scalar micro-program, which is the shape XLA's own \
fusion chooses for itself",
));
}
#[allow(unreachable_patterns)]
other => {
return Err(self.err(format!(
"tape instruction `{}` has no lowering: the tape has grown and the \
XLA emitter must follow",
other.opcode()
)));
}
}
Ok(())
}
fn define(&mut self, slot: SlotId, v: XlaOp) {
self.slots[slot as usize] = Some(v);
}
fn emit_interp(
&self,
tbl: &InterpTable,
dims: &[usize],
x: &XlaOp,
y: Option<&XlaOp>,
) -> R<XlaOp> {
match tbl.kind {
InterpKind::Linear => {
let blend = self.interp_blend(&tbl.table, &tbl.axis_x, dims, x)?;
let n = tbl.axis_x.len();
let lo_knot = self.splat(tbl.axis_x[0], dims)?;
let hi_knot = self.splat(tbl.axis_x[n - 1], dims)?;
let lo_val = self.splat(tbl.table[0], dims)?;
let hi_val = self.splat(tbl.table[n - 1], dims)?;
let above = self.wrap(x.ge(&hi_knot), "interp.linear: x >= axis[N]")?;
let inner =
self.wrap(above.select(&hi_val, &blend), "interp.linear: high clamp")?;
let below = self.wrap(x.le(&lo_knot), "interp.linear: x <= axis[1]")?;
self.wrap(below.select(&lo_val, &inner), "interp.linear: low clamp")
}
InterpKind::Bilinear => {
let y = y.ok_or_else(|| self.err("interp.bilinear without a `y` operand"))?;
let ny = tbl.axis_y.len();
let xq = self.interp_clamp_query(&tbl.axis_x, dims, x)?;
let yq = self.interp_clamp_query(&tbl.axis_y, dims, y)?;
let i = self.interp_cell(&tbl.axis_x, dims, &xq)?;
let j = self.interp_cell(&tbl.axis_y, dims, &yq)?;
let ax_lo = self.interp_take_at(&tbl.axis_x, &i)?;
let ax_hi = self.interp_take_next(&tbl.axis_x, &i)?;
let ay_lo = self.interp_take_at(&tbl.axis_y, &j)?;
let ay_hi = self.interp_take_next(&tbl.axis_y, &j)?;
let wx = self.interp_weight(&xq, &ax_lo, &ax_hi)?;
let wy = self.interp_weight(&yq, &ay_lo, &ay_hi)?;
let nyc = self.ci32(ny as i32, dims)?;
let one = self.ci32(1, dims)?;
let irow = self.wrap(i.mul_(&nyc), "interp.bilinear: i * ny")?;
let irow_n = self.wrap(irow.add_(&nyc), "interp.bilinear: (i+1) * ny")?;
let jp1 = self.wrap(j.add_(&one), "interp.bilinear: j + 1")?;
let c00 = self.interp_take_at(&tbl.table, &self.wrap(irow.add_(&j), "c00")?)?;
let c10 = self.interp_take_at(&tbl.table, &self.wrap(irow_n.add_(&j), "c10")?)?;
let c01 = self.interp_take_at(&tbl.table, &self.wrap(irow.add_(&jp1), "c01")?)?;
let c11 = self.interp_take_at(&tbl.table, &self.wrap(irow_n.add_(&jp1), "c11")?)?;
let row_j = self.interp_lerp(&c00, &c10, &wx)?;
let row_jp1 = self.interp_lerp(&c01, &c11, &wx)?;
self.interp_lerp(&row_j, &row_jp1, &wy)
}
InterpKind::SearchSorted => {
let xs = &tbl.axis_x;
let n = xs.len();
let below = self.interp_count(xs, dims, x, CountCmp::AxisLtQuery)?;
let one = self.ci32(1, dims)?;
let idx = self.wrap(below.add_(&one), "searchsorted: count + 1")?;
let idx = self.wrap(
idx.convert(PrimitiveType::F64),
"searchsorted: index -> f64",
)?;
if n == 0 {
return Ok(idx);
}
let past_end = self.splat((n as i64 + 1) as f64, dims)?;
let is_nan = self.wrap(x.ne(x), "searchsorted: x is NaN")?;
self.wrap(is_nan.select(&past_end, &idx), "searchsorted: NaN query")
}
}
}
fn interp_blend(&self, table: &[f64], axis: &[f64], dims: &[usize], q: &XlaOp) -> R<XlaOp> {
let i = self.interp_cell(axis, dims, q)?;
let a_lo = self.interp_take_at(axis, &i)?;
let a_hi = self.interp_take_next(axis, &i)?;
let t_lo = self.interp_take_at(table, &i)?;
let t_hi = self.interp_take_next(table, &i)?;
let w = self.interp_weight(q, &a_lo, &a_hi)?;
self.interp_lerp(&t_lo, &t_hi, &w)
}
fn interp_clamp_query(&self, axis: &[f64], dims: &[usize], q: &XlaOp) -> R<XlaOp> {
let n = axis.len();
let lo = self.splat(axis[0], dims)?;
let hi = self.splat(axis[n - 1], dims)?;
let above = self.wrap(q.ge(&hi), "interp: q >= axis[N]")?;
let inner = self.wrap(above.select(&hi, q), "interp: clamp high")?;
let below = self.wrap(q.le(&lo), "interp: q <= axis[1]")?;
self.wrap(below.select(&lo, &inner), "interp: clamp low")
}
fn interp_cell(&self, axis: &[f64], dims: &[usize], q: &XlaOp) -> R<XlaOp> {
let n = axis.len();
let count = self.interp_count(&axis[..n - 1], dims, q, CountCmp::AxisLeQuery)?;
let one = self.ci32(1, dims)?;
let cand = self.wrap(count.sub_(&one), "interp: count - 1")?;
let lo = self.ci32(0, dims)?;
let hi = self.ci32(n as i32 - 2, dims)?;
self.wrap(cand.clamp(&lo, &hi), "interp: clamp cell index")
}
fn interp_count(&self, knots: &[f64], dims: &[usize], q: &XlaOp, cmp: CountCmp) -> R<XlaOp> {
if knots.is_empty() {
return self.ci32(0, dims);
}
let mut wide: Vec<i64> = dims.iter().map(|&d| d as i64).collect();
wide.push(knots.len() as i64);
let keep: Vec<i64> = (0..dims.len() as i64).collect();
let qb = self.wrap(q.broadcast_in_dim(&wide, &keep), "interp: broadcast query")?;
let kv = self.wrap(self.b.constant_r1(knots), "interp: knot vector")?;
let kb = self.wrap(
kv.broadcast_in_dim(&wide, &[dims.len() as i64]),
"interp: broadcast knots",
)?;
let pred = match cmp {
CountCmp::AxisLeQuery => self.wrap(kb.le(&qb), "interp: axis <= q")?,
CountCmp::AxisLtQuery => self.wrap(kb.lt(&qb), "interp: axis < q")?,
};
let ones = self.wrap(pred.convert(PrimitiveType::S32), "interp: pred -> s32")?;
self.wrap(
ones.reduce_sum(&[dims.len() as i64], false),
"interp: count knots",
)
}
fn interp_take_at(&self, values: &[f64], idx: &XlaOp) -> R<XlaOp> {
let v = self.wrap(self.b.constant_r1(values), "interp: table constant")?;
self.wrap(v.take(idx, 0), "interp: table gather")
}
fn interp_take_next(&self, values: &[f64], idx: &XlaOp) -> R<XlaOp> {
let dims = self.dims(idx)?;
let one = self.ci32(1, &dims)?;
let next = self.wrap(idx.add_(&one), "interp: idx + 1")?;
self.interp_take_at(values, &next)
}
fn interp_weight(&self, q: &XlaOp, lo: &XlaOp, hi: &XlaOp) -> R<XlaOp> {
let num = self.wrap(q.sub_(lo), "interp: q - axis[i]")?;
let den = self.wrap(hi.sub_(lo), "interp: axis[i+1] - axis[i]")?;
self.wrap(num.div_(&den), "interp: weight")
}
fn interp_lerp(&self, a: &XlaOp, b: &XlaOp, w: &XlaOp) -> R<XlaOp> {
let d = self.wrap(b.sub_(a), "interp: b - a")?;
let s = self.wrap(w.mul_(&d), "interp: w * (b - a)")?;
self.wrap(a.add_(&s), "interp: a + w * (b - a)")
}
fn splat(&self, v: f64, dims: &[usize]) -> R<XlaOp> {
let c = self.c(v)?;
self.to_shape(&c, dims)
}
fn ci32(&self, v: i32, dims: &[usize]) -> R<XlaOp> {
let c = self
.b
.c0(v)
.map_err(|e| self.err(format!("s32 constant: {e}")))?;
if dims.is_empty() {
return Ok(c);
}
let d: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
self.wrap(c.broadcast(&d), "interp: broadcast s32 constant")
}
fn emit_poly_area(&self, a: &SrcRef, b: &SrcRef, geom: u32, out: SlotId) -> R<XlaOp> {
let spec = &self.prog.geoms[geom as usize];
let ta = self.const_table(a)?;
let tb = self.const_table(b)?;
let (sa, sb) = (rm_strides(&ta.shape), rm_strides(&tb.shape));
let dims = self.out_dims(out);
let mut areas = vec![0.0f64; dims.iter().product::<usize>().max(1)];
unsafe {
super::geom::run_poly_area(
spec,
&super::geom::RingTable {
ptr: ta.values.as_ptr(),
shape: &ta.shape,
strides: &sa,
},
&super::geom::RingTable {
ptr: tb.values.as_ptr(),
shape: &tb.shape,
strides: &sb,
},
areas.as_mut_ptr(),
);
}
let flat = self.wrap(self.b.constant_r1(&areas[..]), "polygon area: literal")?;
let d: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
self.wrap(flat.reshape(&d), "polygon area: reshape")
}
fn const_table(&self, s: &SrcRef) -> R<&ConstArrayData> {
let not_const = || {
self.err(
"polygon_intersection_area: a ring table that is not a compile-time \
constant (it depends on a parameter, the state or an observed) has no \
lowering: XLA has no polygon clip",
)
};
let SrcRef::Slot(id) = s else {
return Err(not_const());
};
self.prog
.instrs
.iter()
.find_map(|i| match i {
Instr::ConstArray { data, out } if out == id => {
Some(&self.prog.const_data[*data as usize])
}
_ => None,
})
.ok_or_else(not_const)
}
fn emit_index_gather(&self, src: &SrcRef, idx: &Operand, spec: u32) -> R<XlaOp> {
let spec = &self.prog.index_gathers[spec as usize];
let s = self.src(src)?;
let have = self.dims(&s)?;
if have != spec.src_shape.as_slice() {
return Err(self.err(format!(
"indexed gather source has shape {have:?} but the spec expects {:?}",
&spec.src_shape[..]
)));
}
let dims: Vec<usize> = spec.shape.to_vec();
let d64: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
let total: i64 = spec.src_shape.iter().product::<usize>() as i64;
let flat = self.wrap(s.reshape(&[total]), "indexed gather: flatten source")?;
let strides = rm_strides(&spec.src_shape);
let data_d = spec.data_axis();
let n = spec.src_shape[data_d] as f64;
let iv = self.operand(idx)?;
let iv = self.to_shape(&iv, &dims)?;
let r = self.wrap(iv.round(), "indexed gather: round")?;
let nan = self.wrap(r.ne(&r), "indexed gather: isnan")?;
let zero = self.splat(0.0, &dims)?;
let r = self.wrap(nan.select(&zero, &r), "indexed gather: NaN reads as 0")?;
let top = self.splat(n + 1.0, &dims)?;
let r = self.wrap(
r.max(&zero).and_then(|x| x.min(&top)),
"indexed gather: pin",
)?;
let one_based = self.wrap(r.convert(PrimitiveType::S64), "indexed gather: convert")?;
let splat_i = |v: i64| -> R<XlaOp> {
let c = self.ci(v)?;
self.to_shape(&c, &dims)
};
let (one, zero_i, n_i, last_i) = (
splat_i(1)?,
splat_i(0)?,
splat_i(n as i64)?,
splat_i(n as i64 - 1)?,
);
let valid = self.wrap(
one_based
.ge(&one)
.and_then(|lo| one_based.le(&n_i).and_then(|hi| lo.and(&hi))),
"indexed gather: in range",
)?;
let pos = self.wrap(
one_based
.sub_(&one)
.and_then(|p| p.max(&zero_i))
.and_then(|p| p.min(&last_i)),
"indexed gather: position",
)?;
let stride_data = splat_i(strides[data_d])?;
let mut off = self.wrap(pos.mul_(&stride_data), "indexed gather: data offset")?;
for (d, ax) in spec.axes.iter().enumerate() {
let term = match *ax {
GatherAxis::Data => continue,
GatherAxis::Fixed(i) => splat_i(i as i64 * strides[d])?,
GatherAxis::Affine { axis, off: k } => {
let io = self.wrap(
self.b.iota(ElementType::S64, &d64, axis as i64),
"indexed gather: iota",
)?;
let (k, st) = (splat_i(k)?, splat_i(strides[d])?);
self.wrap(
io.add_(&k).and_then(|x| x.mul_(&st)),
"indexed gather: affine offset",
)?
}
};
off = self.wrap(off.add_(&term), "indexed gather: offset")?;
}
let g = self.wrap(flat.take(&off, 0), "indexed gather: take")?;
self.wrap(valid.select(&g, &zero), "indexed gather: ghost")
}
fn emit_gather(&self, plan: &GatherPlan, src: &XlaOp) -> R<XlaOp> {
let mut cur = src.clone();
for &(d, i0) in &plan.fixed_desc {
let dims = self.dims(&cur)?;
if d >= dims.len() {
return Err(self.err("gather plan fixes an axis the source does not have"));
}
let mut nd: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
nd.remove(d);
cur = self.wrap(
cur.slice_in_dim(i0 as i64, i0 as i64 + 1, 1, d as i64)
.and_then(|s| s.reshape(&nd)),
"gather: fixed axis",
)?;
}
if plan.perm.len() > 1 && !plan.perm.iter().enumerate().all(|(i, &p)| i == p) {
let perm: Vec<i64> = plan.perm.iter().map(|&x| x as i64).collect();
cur = self.wrap(cur.transpose(&perm), "gather: transpose")?;
}
let out_ndim = plan.shape.len();
let mapped_axes: Vec<i64> = (0..out_ndim)
.filter(|&a| plan.mapped[a])
.map(|a| a as i64)
.collect();
let cur_dims = self.dims(&cur)?;
if cur_dims.len() != mapped_axes.len() {
return Err(self.err(format!(
"gather plan maps {} source axes but {} output axes are marked mapped",
cur_dims.len(),
mapped_axes.len()
)));
}
if out_ndim > 0 {
let mut bshape: Vec<i64> = Vec::with_capacity(out_ndim);
let mut k = 0usize;
for a in 0..out_ndim {
if plan.mapped[a] {
bshape.push(cur_dims[k] as i64);
k += 1;
} else {
bshape.push(plan.shape[a] as i64);
}
}
if bshape.len() != cur_dims.len()
|| !mapped_axes
.iter()
.enumerate()
.all(|(i, &a)| a as usize == i)
{
cur = self.wrap(
cur.broadcast_in_dim(&bshape, &mapped_axes),
"gather: broadcast_in_dim",
)?;
}
}
self.emit_segments(&cur, plan, 0)
}
fn emit_segments(&self, cur: &XlaOp, plan: &GatherPlan, d: usize) -> R<XlaOp> {
if d == plan.shape.len() {
return Ok(cur.clone());
}
let extent = plan.shape[d];
let mut segs: Vec<(usize, usize, usize)> = plan.segs[d].to_vec();
segs.sort_by_key(|s| s.0);
if segs.len() == 1 && segs[0] == (0, extent, 0) {
let cd = self.dims(cur)?;
if cd[d] == extent {
return self.emit_segments(cur, plan, d + 1);
}
}
let cd = self.dims(cur)?;
let piece_zeros = |len: usize| -> R<XlaOp> {
let mut shape: Vec<usize> = cd.clone();
shape[d] = len;
for (e, s) in shape.iter_mut().enumerate().skip(d + 1) {
*s = plan.shape[e];
}
self.zeros(&shape)
};
let mut pieces: Vec<XlaOp> = Vec::new();
let mut next = 0usize;
for (o, l, s) in segs {
if o < next {
return Err(self.err("gather plan segments overlap along an axis"));
}
if o > next {
pieces.push(piece_zeros(o - next)?);
}
let sl = self.wrap(
cur.slice_in_dim(s as i64, (s + l) as i64, 1, d as i64),
"gather: segment slice",
)?;
pieces.push(self.emit_segments(&sl, plan, d + 1)?);
next = o + l;
}
if next > extent {
return Err(self.err("gather plan segments run past the output extent"));
}
if next < extent {
pieces.push(piece_zeros(extent - next)?);
}
match pieces.len() {
0 => Err(self.err("gather plan has no segments and no extent along an axis")),
1 => Ok(pieces.remove(0)),
_ => {
let head = pieces.remove(0);
self.wrap(
head.concat_in_dim(&pieces, d as i64),
"gather: concat_in_dim",
)
}
}
}
fn emit_dy_write(&mut self, w: &DyWrite) -> R<()> {
if w.scatter.is_some() {
return Err(self.err(
"a state derivative whose left-hand side is not a constant shift of its \
output indices (a per-cell dy scatter) has no XLA lowering",
));
}
let vi = match w.scalar_flat {
Some(flat) => self.var_owning_slot(flat)?,
None => {
let sv = &self.prog.state_vars[w.var as usize];
*self.var_ix.get(&sv.name).ok_or_else(|| {
self.err(format!("dy write names unknown variable {}", sv.name))
})?
}
};
let (var_name, vs) = &self.var_order[vi];
let var_name = var_name.clone();
let shape: Vec<usize> = vs.shape.clone();
let var_offset = vs.flat_offset;
let val = self.slots[w.slot as usize]
.clone()
.ok_or_else(|| self.err("dy-write slot is undefined"))?;
let val_dims = self.dims(&val)?;
let cur = match &self.blocks[vi] {
Some(b) => b.clone(),
None => self.zeros(&shape)?,
};
let next = match w.scalar_flat {
Some(flat) => {
if !val_dims.is_empty() {
return Err(self.err("scalar dy write with an array value"));
}
if shape.is_empty() {
val
} else {
let mut rem = flat - var_offset;
let mut multi = vec![0usize; shape.len()];
for d in 0..shape.len() {
multi[d] = rem % shape[d];
rem /= shape[d];
}
if rem != 0 {
return Err(self.err("scalar dy write lands past its variable block"));
}
let ones: Vec<i64> = shape.iter().map(|_| 1i64).collect();
let upd = self.wrap(val.reshape(&ones), "dy write: reshape scalar")?;
let starts: Vec<XlaOp> = multi
.iter()
.map(|&x| self.ci(x as i64))
.collect::<R<Vec<_>>>()?;
self.wrap(
cur.dynamic_update_slice(&upd, &starts),
"dy write: dynamic_update_slice",
)?
}
}
None => {
if shape.is_empty() {
if !val_dims.is_empty() {
return Err(self.err("array dy write into a 0-d variable"));
}
val
} else {
if val_dims.len() != shape.len() {
return Err(self.err(format!(
"dy write value has rank {} but variable {var_name} has rank {}",
val_dims.len(),
shape.len()
)));
}
let starts: Vec<XlaOp> = w
.dest_lo
.iter()
.map(|&x| self.ci(x as i64))
.collect::<R<Vec<_>>>()?;
self.wrap(
cur.dynamic_update_slice(&val, &starts),
"dy write: dynamic_update_slice",
)?
}
}
};
self.blocks[vi] = Some(next);
Ok(())
}
fn reduce_computation(&self, op: BinCode) -> R<XlaComputation> {
let rb = XlaBuilder::new("reduce_kernel");
let a = rb
.parameter(0, f64::TY, &[], "a")
.map_err(|e| self.err(format!("reduce kernel parameter a: {e}")))?;
let b = rb
.parameter(1, f64::TY, &[], "b")
.map_err(|e| self.err(format!("reduce kernel parameter b: {e}")))?;
let body = self.bin(op, &a, &b)?;
body.build()
.map_err(|e| self.err(format!("reduce kernel build: {e}")))
}
fn emit_table_gather(&self, src: &XlaOp, dims: &[usize], pos: &[u32]) -> R<XlaOp> {
let n: usize = dims.iter().product::<usize>().max(1);
let flat = self.wrap(src.reshape(&[n as i64]), "table gather: flatten")?;
let at: Vec<i64> = pos
.iter()
.map(|&p| if p == GATHER_GHOST { 0 } else { p as i64 })
.collect();
let at = self.wrap(self.b.constant_r1(&at[..]), "table gather: positions")?;
let taken = self.wrap(flat.take(&at, 0), "table gather: take")?;
if !pos.contains(&GATHER_GHOST) {
return Ok(taken);
}
let live: Vec<f64> = pos
.iter()
.map(|&p| if p == GATHER_GHOST { 0.0 } else { 1.0 })
.collect();
let live = self.wrap(self.b.constant_r1(&live[..]), "table gather: live mask")?;
let z = self.splat_like(&taken, 0.0)?;
let keep = self.wrap(live.ne(&z), "table gather: live != 0")?;
self.wrap(keep.select(&taken, &z), "table gather: ghost")
}
fn emit_seg_reduce(
&self,
op: BinCode,
init: f64,
terms: &XlaOp,
mask: Option<&XlaOp>,
rows: &[u32],
) -> R<XlaOp> {
let n_out = rows.len() - 1;
let m = rows[n_out] as usize;
let base = {
let c = self.c(init)?;
self.wrap(c.broadcast(&[n_out as i64]), "segmented reduce: identity")?
};
if m == 0 {
return Ok(base);
}
let terms = match mask {
None => terms.clone(),
Some(mk) => {
let z = self.splat_like(mk, 0.0)?;
let keep = self.wrap(mk.ne(&z), "segmented reduce: mask != 0")?;
let id = self.splat_like(terms, init)?;
self.wrap(keep.select(terms, &id), "segmented reduce: mask")?
}
};
let mut cell: Vec<i64> = Vec::with_capacity(m);
for c in 0..n_out {
cell.extend(std::iter::repeat_n(
c as i64,
(rows[c + 1] - rows[c]) as usize,
));
}
let cell = self.wrap(
self.b
.constant_r1(&cell[..])
.and_then(|c| c.reshape(&[m as i64, 1])),
"segmented reduce: cell ids",
)?;
let comp = self.reduce_computation(op)?;
self.wrap(
base.scatter(&cell, &terms, &comp, &[], &[0], &[0], 1),
"segmented reduce: scatter",
)
}
fn emit_scan_loop(
&self,
op: BinCode,
init: f64,
src: &XlaOp,
dims: &[usize],
ax: usize,
inclusive: bool,
reverse: bool,
) -> R<XlaOp> {
use xla::Shape;
let full: Vec<i64> = dims.iter().map(|&x| x as i64).collect();
let mut plane = full.clone();
plane[ax] = 1;
let carried = Shape::tuple(vec![
Shape::array::<i64>(vec![]),
Shape::array::<f64>(full.clone()),
Shape::array::<f64>(plane.clone()),
Shape::array::<f64>(full.clone()),
]);
let n = full[ax];
let cb = XlaBuilder::new("scan_cond");
let st = self.wrap(cb.parameter_s(0, &carried, "st"), "scan cond: parameter")?;
let k = self.wrap(st.get_tuple_element(0), "scan cond: k")?;
let bound = self.wrap(cb.c0(n), "scan cond: bound")?;
let cond = self.wrap(k.lt(&bound), "scan cond: k < n")?;
let cond = cond
.build()
.map_err(|e| self.err(format!("scan cond build: {e}")))?;
let bb = XlaBuilder::new("scan_body");
let st = self.wrap(bb.parameter_s(0, &carried, "st"), "scan body: parameter")?;
let part = |i: i64| self.wrap(st.get_tuple_element(i), "scan body: unpack");
let (k, s, acc, out) = (part(0)?, part(1)?, part(2)?, part(3)?);
let zero = self.wrap(bb.c0(0i64), "scan body: zero")?;
let pos = if reverse {
let last = self.wrap(bb.c0(n - 1), "scan body: last")?;
self.wrap(last.sub_(&k), "scan body: n - 1 - k")?
} else {
k.clone()
};
let starts: Vec<XlaOp> = (0..full.len())
.map(|d| if d == ax { pos.clone() } else { zero.clone() })
.collect();
let pl = self.wrap(s.dynamic_slice(&starts, &plane), "scan body: plane")?;
let next = self.bin(op, &acc, &pl)?;
let written = if inclusive { &next } else { &acc };
let out = self.wrap(
out.dynamic_update_slice(written, &starts),
"scan body: write plane",
)?;
let one = self.wrap(bb.c0(1i64), "scan body: one")?;
let k1 = self.wrap(k.add_(&one), "scan body: k + 1")?;
let body = self.wrap(bb.tuple(&[k1, s, next, out]), "scan body: pack")?;
let body = body
.build()
.map_err(|e| self.err(format!("scan body build: {e}")))?;
let acc0 = {
let c = self.c(init)?;
self.wrap(c.broadcast(&plane), "scan: initial accumulator")?
};
let start = self.wrap(
self.b
.tuple(&[self.ci(0)?, src.clone(), acc0, self.zeros(dims)?]),
"scan: initial state",
)?;
let res = self.wrap(XlaOp::while_(cond, body, start), "scan: while")?;
self.wrap(res.get_tuple_element(3), "scan: result")
}
fn var_owning_slot(&self, flat: usize) -> R<usize> {
for (i, (_, vs)) in self.var_order.iter().enumerate() {
let n: usize = vs.shape.iter().product::<usize>().max(1);
if flat >= vs.flat_offset && flat < vs.flat_offset + n {
return Ok(i);
}
}
Err(self.err(format!(
"scalar dy write addresses flat slot {flat}, which lies in no variable block"
)))
}
fn assemble_du(&mut self) -> R<XlaOp> {
self.cur_rule = "<program>".to_string();
let mut flat: Vec<XlaOp> = Vec::with_capacity(self.var_order.len());
for (i, (_, vs)) in self.var_order.iter().enumerate() {
let n: usize = vs.shape.iter().product::<usize>().max(1);
let blk = match &self.blocks[i] {
Some(b) => b.clone(),
None => self.zeros(&vs.shape)?,
};
let f = if vs.shape.is_empty() {
self.wrap(blk.reshape(&[1]), "du: reshape scalar block")?
} else {
let perm: Vec<i64> = (0..vs.shape.len() as i64).rev().collect();
self.wrap(
blk.transpose(&perm).and_then(|b| b.reshape(&[n as i64])),
"du: relabel block",
)?
};
flat.push(f);
}
match flat.len() {
0 => self.zeros(&[0]),
1 => Ok(flat.remove(0)),
_ => {
let head = flat.remove(0);
self.wrap(head.concat_in_dim(&flat, 0), "du: concat")
}
}
}
}