use super::*;
use crate::compile_error::CompileError;
use crate::faq::effective_reduce_kind;
use crate::types::{ExpressionNode, JoinClause};
pub(super) type RangeVec = SmallVec<[(i64, i64); 4]>;
pub(super) fn derived_extent(from_faq: &str, ctx: &EvalCtx) -> i64 {
if let Some(&n) = ctx.derived_extents.get(from_faq) {
return n;
}
match ctx.derived_rings.borrow().get(from_faq) {
Some(ring) if ring.ndim() >= 1 => (ring.shape()[0] as i64 - 1).max(0),
_ => 0,
}
}
pub(super) fn eval(expr: &Expr, ctx: &mut EvalCtx) -> Value {
match expr {
Expr::Number(n) => Value::Scalar(crate::precision::round(*n)),
Expr::Integer(n) => Value::Scalar(crate::precision::round(*n as f64)),
Expr::Variable(name) => lookup_variable(name, ctx),
Expr::Operator(node) => eval_op(node, ctx),
}
}
pub(super) fn lookup_variable(name: &str, ctx: &EvalCtx) -> Value {
let prec = crate::precision::active();
if name == "t" {
return Value::Scalar(prec.round(ctx.t));
}
if let Some(scope) = ctx.recur
&& scope.name == name
{
latch_gather_fault(format!(
"E_TREEWALK_RECUR_UNAVAILABLE: '{name}' is read BARE inside its own recurrence \
definition — the whole array does not exist during the sweep. Read it through \
`index` at a strictly earlier position instead (esm-spec §4.3.1.1)."
));
return Value::Scalar(f64::NAN);
}
if let Some(v) = ctx.loop_binds.get(name) {
return Value::Scalar(*v as f64);
}
if let Some(a) = ctx.state_arrays.get(name) {
return if a.ndim() == 0 {
Value::Scalar(prec.round(a[IxDyn(&[])]))
} else {
Value::Array(Box::new(a.clone()))
};
}
if let Some(a) = ctx.observed_arrays.get(name) {
return if a.ndim() == 0 {
Value::Scalar(prec.round(a[IxDyn(&[])]))
} else {
Value::Array(Box::new(a.clone()))
};
}
if let Some(i) = ctx.param_names.iter().position(|p| p == name) {
return Value::Scalar(prec.round(ctx.params[i]));
}
if let Some(a) = ctx.forcing.borrow().get(name) {
return if a.ndim() == 0 {
Value::Scalar(prec.round(a[IxDyn(&[])]))
} else {
Value::Array(Box::new(a.clone()))
};
}
latch_unbound_read(name, ctx.declared);
Value::Scalar(f64::NAN)
}
pub(super) fn latch_unbound_read(name: &str, declared: &HashSet<String>) {
if declared.contains(name) {
latch_gather_fault(format!(
"E_TREEWALK_UNRESOLVED_ORDER: '{name}' IS declared in this model, but nothing had \
produced a value for it at the point this expression was evaluated. For an observed \
that means its defining rule had not run yet — the usual cause is a dependency \
cycle among observed variables, which no evaluation order satisfies (esm-spec \
§4.9.6). Run `esm validate`: it reports the cycle as `observed_cycle` and names the \
observeds on it. This is NOT an undeclared name — see E_TREEWALK_UNBOUND_NAME for \
that (CONFORMANCE_SPEC §5.23)."
));
return;
}
latch_gather_fault(format!(
"E_TREEWALK_UNBOUND_NAME: '{name}' is referenced in an expression but bound by \
NOTHING in scope — it is not the independent variable, a loop index, a state, an \
observed, a parameter, or a forcing channel. A name declared nowhere in the \
document is a structural error (`esm validate` reports it against the equation); \
reaching evaluation means one route skipped that gate. Fail-closed per \
CONFORMANCE_SPEC §5.23: never a NaN sentinel, which `max(x, floor)` or any \
comparison would launder into a plausible number by dropping the operand."
));
}
pub(super) fn latch_forcing_shape_mismatch(name: &str, built: &[usize], held: &[usize]) {
latch_gather_fault(format!(
"the forcing buffer holds '{name}' with shape {held:?}, but the compiled program \
reads it as {built:?} (the shape the buffer held, or else the variable declared, \
when the program was built). A compiled program's boxes are fixed at build: a \
forcing field that changes shape between refreshes cannot be read by it."
));
}
#[inline]
pub(super) fn set_bind(binds: &mut IdxMap, name: &str, val: i64) {
if let Some(slot) = binds.get_mut(name) {
*slot = val;
} else {
binds.insert(name.to_string(), val);
}
}
#[must_use]
pub fn is_evaluable_op(op: &str) -> bool {
matches!(
op,
"+" | "-" | "*" | "/" | "^" | "pow" | "neg"
| "exp" | "log" | "log10" | "sqrt" | "abs" | "sign" | "floor" | "ceil"
| "sin" | "cos" | "tan" | "asin" | "acos" | "atan"
| "sinh" | "cosh" | "tanh" | "asinh" | "acosh" | "atanh"
| "atan2" | "min" | "max"
| "==" | "!=" | "<" | "<=" | ">" | ">=" | "and" | "or" | "not"
| "ifelse"
| "D" | "Pre" | "const" | "true" | "false"
| "index" | "faq" | "makearray" | "reshape" | "transpose" | "concat"
| "broadcast" | "intersect_polygon" | "polygon_intersection_area"
| "fn"
| crate::precision_infer::MARKER_OP
)
}
pub fn check_evaluable(expr: &Expr) -> Result<(), CompileError> {
check_no_spatial_ops(expr)?;
check_evaluable_ops(expr)?;
crate::precision::check_f32_supported(expr)
}
fn check_evaluable_ops(expr: &Expr) -> Result<(), CompileError> {
let Expr::Operator(node) = expr else {
return Ok(());
};
if !is_evaluable_op(&node.op) {
return Err(CompileError::UnevaluableOperatorError {
op: node.op.clone(),
});
}
let mut first_err: Option<CompileError> = None;
node.for_each_child(&mut |child| {
if first_err.is_none()
&& let Err(e) = check_evaluable_ops(child)
{
first_err = Some(e);
}
});
match first_err {
Some(e) => Err(e),
None => Ok(()),
}
}
pub(crate) fn eval_scalar_expression(
expr: &Expr,
params: &[f64],
param_names: &[String],
t: f64,
) -> Result<f64, CompileError> {
check_scalar_evaluable(expr)?;
let value = eval_expression(expr, &HashMap::new(), params, param_names, t)?;
value
.as_scalar()
.ok_or_else(|| CompileError::InterpreterBuildError {
details: "the expression is array-valued; a scalar evaluation has one number \
to return"
.to_string(),
})
}
pub(crate) fn check_scalar_evaluable(expr: &Expr) -> Result<(), CompileError> {
check_no_spatial_ops(expr)?;
check_evaluable_ops(expr)?;
check_scalar_ops(expr)?;
crate::precision::check_f32_supported(expr)
}
fn check_scalar_ops(expr: &Expr) -> Result<(), CompileError> {
let Expr::Operator(node) = expr else {
return Ok(());
};
let no_scalar_value = match node.op.as_str() {
"D"
| "index"
| "faq"
| "makearray"
| "reshape"
| "transpose"
| "concat"
| "broadcast"
| "intersect_polygon"
| "polygon_intersection_area" => true,
"const" => !node
.value
.as_ref()
.is_some_and(serde_json::Value::is_number),
_ => false,
};
if no_scalar_value {
return Err(CompileError::UnevaluableOperatorError {
op: node.op.clone(),
});
}
let is_fn = node.op == "fn";
let mut first_err: Option<CompileError> = None;
node.for_each_child(&mut |child| {
let table_arg = is_fn && matches!(child, Expr::Operator(c) if c.op == "const");
if first_err.is_none()
&& !table_arg
&& let Err(e) = check_scalar_ops(child)
{
first_err = Some(e);
}
});
match first_err {
Some(e) => Err(e),
None => Ok(()),
}
}
pub(super) fn eval_op(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
eval_op_named(node.op.as_str(), node, ctx)
}
fn eval_op_named(op: &str, node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
match op {
"+" | "-" | "*" | "/" | "^" => eval_arith(op, &node.args, ctx),
"pow" => eval_arith("^", &node.args, ctx),
"neg" => {
if node.args.len() != 1 {
return Value::Scalar(f64::NAN);
}
negate(eval(&node.args[0], ctx))
}
crate::precision_infer::MARKER_OP => {
if node.args.len() != 1 {
return Value::Scalar(f64::NAN);
}
match crate::precision_infer::marker_precision(node) {
Some(p) => {
let _guard = crate::precision::enter(p);
eval(&node.args[0], ctx)
}
None => eval(&node.args[0], ctx),
}
}
"exp" | "log" | "log10" | "sqrt" | "abs" | "sign" | "floor" | "ceil" | "sin" | "cos"
| "tan" | "asin" | "acos" | "atan" | "sinh" | "cosh" | "tanh" | "asinh" | "acosh"
| "atanh" => eval_unary(op, &node.args, ctx),
"atan2" => eval_binary(op, &node.args, ctx),
"min" | "max" => eval_arith(op, &node.args, ctx),
"==" | "!=" | "<" | "<=" | ">" | ">=" => {
if node.args.len() != 2 {
return Value::Scalar(f64::NAN);
}
eval_binary(op, &node.args, ctx)
}
"and" | "or" => eval_arith(op, &node.args, ctx),
"not" => eval_unary(op, &node.args, ctx),
"ifelse" => eval_ifelse(node, ctx),
"D" => Value::Scalar(f64::NAN),
"Pre" => {
if node.args.is_empty() {
Value::Scalar(f64::NAN)
} else {
eval(&node.args[0], ctx)
}
}
"const" => eval_const_memo(node, ctx.const_lits),
"index" => eval_index(node, ctx),
"faq" => eval_faq(node, ctx),
"intersect_polygon" => eval_intersect_polygon(node, ctx),
"polygon_intersection_area" => eval_polygon_intersection_area(node, ctx),
"makearray" => eval_makearray(node, ctx),
"reshape" => eval_reshape(node, ctx),
"transpose" => eval_transpose(node, ctx),
"concat" => eval_concat(node, ctx),
"broadcast" => eval_broadcast(node, ctx),
"fn" => eval_fn(node, ctx),
"true" => Value::Scalar(1.0),
"false" => Value::Scalar(0.0),
other => unreachable!(
"operator '{other}' reached eval_op without an evaluation rule; \
every entry point must gate with check_evaluable() first"
),
}
}
pub(super) fn eval_fn(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
use crate::registered_functions::{ClosedArg, evaluate_closed_function};
let Some(name) = node.name.as_deref() else {
return Value::Scalar(f64::NAN);
};
let vals: ValVec = node.args.iter().map(|a| eval(a, ctx)).collect();
let strict = crate::lower_table_lookup::strict_table(node);
if name == "interp.linear"
&& vals.len() == 3
&& let Value::Array(q) = &vals[2]
{
let table: Vec<f64> = value_flat(&vals[0]);
let axis: Vec<f64> = value_flat(&vals[1]);
let out = q.mapv(|x| {
if let Some(id) = strict
&& let Some(fault) = crate::lower_table_lookup::out_of_bounds_fault(id, 1, &axis, x)
{
latch_gather_fault(fault);
return f64::NAN;
}
let call = [
ClosedArg::Array(table.clone()),
ClosedArg::Array(axis.clone()),
ClosedArg::Scalar(x),
];
evaluate_closed_function("interp.linear", &call)
.map(|v| v.as_f64())
.unwrap_or(f64::NAN)
});
return Value::Array(Box::new(out));
}
let mut args: Vec<ClosedArg> = Vec::with_capacity(vals.len());
for v in vals {
let arg = match v {
Value::Scalar(s) => ClosedArg::Scalar(s),
Value::Array(arr) => match arr.ndim() {
0 => ClosedArg::Scalar(arr[IxDyn(&[])]),
1 => ClosedArg::Array(arr.iter().copied().collect()),
2 => {
let (rows, cols) = (arr.shape()[0], arr.shape()[1]);
let mut out = Vec::with_capacity(rows);
for i in 0..rows {
let mut row = Vec::with_capacity(cols);
for j in 0..cols {
row.push(arr[IxDyn(&[i, j])]);
}
out.push(row);
}
ClosedArg::Array2D(out)
}
_ => return Value::Scalar(f64::NAN),
},
};
args.push(arg);
}
if let Some(id) = strict
&& let Some(fault) = strict_query_fault(id, name, &args)
{
latch_gather_fault(fault);
return Value::Scalar(f64::NAN);
}
match evaluate_closed_function(name, &args) {
Ok(v) => Value::Scalar(v.as_f64()),
Err(_) => Value::Scalar(f64::NAN),
}
}
fn strict_query_fault(
table_id: &str,
name: &str,
args: &[crate::registered_functions::ClosedArg],
) -> Option<String> {
use crate::lower_table_lookup::out_of_bounds_fault;
use crate::registered_functions::ClosedArg;
let scalar_on = |q: usize, a: usize, which: usize| match (args.get(q), args.get(a)) {
(Some(ClosedArg::Scalar(x)), Some(ClosedArg::Array(axis))) => {
out_of_bounds_fault(table_id, which, axis, *x)
}
_ => None,
};
match name {
"interp.linear" => scalar_on(2, 1, 1),
"interp.bilinear" => scalar_on(3, 1, 1).or_else(|| scalar_on(4, 2, 2)),
"interp.searchsorted" => scalar_on(0, 1, 1),
_ => None,
}
}
pub(super) fn eval_arith(op: &str, args: &[Expr], ctx: &mut EvalCtx) -> Value {
let mut values: ValVec = args.iter().map(|a| eval(a, ctx)).collect();
if op == "-" && values.len() == 1 {
return negate(values.remove(0));
}
if values.iter().all(|v| matches!(v, Value::Scalar(_))) {
let scalars: SmallVec<[f64; 4]> = values
.iter()
.map(|v| match v {
Value::Scalar(s) => *s,
_ => unreachable!(
"eval_arith scalar fast path: operand proven Scalar by the all-scalar guard"
),
})
.collect();
return Value::Scalar(fold_scalar(op, &scalars));
}
let mut acc = values.remove(0);
for v in values {
acc = combine(op, acc, v);
}
acc
}
pub(crate) fn fold_scalar(op: &str, vs: &[f64]) -> f64 {
let Some((first, rest)) = vs.split_first() else {
return f64::NAN;
};
let prec = crate::precision::active();
match op {
"and" => return vs.iter().all(|&v| prec.round(v) != 0.0) as i32 as f64,
"or" => return vs.iter().any(|&v| prec.round(v) != 0.0) as i32 as f64,
_ => {}
}
rest.iter()
.fold(prec.round(*first), |acc, &v| apply_binary(op, acc, v))
}
pub(super) fn negate(v: Value) -> Value {
match v {
Value::Scalar(s) => Value::Scalar(-s),
Value::Array(a) => Value::Array(Box::new(a.mapv(|x| -x))),
}
}
pub(super) fn eval_ifelse(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
if node.args.len() != 3 {
return Value::Scalar(f64::NAN);
}
let cond = match eval(&node.args[0], ctx) {
Value::Scalar(c) => {
return if c != 0.0 {
eval(&node.args[1], ctx)
} else {
eval(&node.args[2], ctx)
};
}
Value::Array(c) => c,
};
let a = eval(&node.args[1], ctx);
let b = eval(&node.args[2], ctx);
let mut target = cond.shape().to_vec();
if let Value::Array(aa) = &a {
target = broadcast_shape(&target, aa.shape());
}
if let Value::Array(bb) = &b {
target = broadcast_shape(&target, bb.shape());
}
let cond_b = broadcast_value(&Value::Array(cond), &target);
let a_b = broadcast_value(&a, &target);
let b_b = broadcast_value(&b, &target);
let mut out = ArrayD::<f64>::zeros(IxDyn(&target));
ndarray::Zip::from(&mut out)
.and(&cond_b)
.and(&a_b)
.and(&b_b)
.for_each(|o, &c, &av, &bv| *o = if c != 0.0 { av } else { bv });
Value::Array(Box::new(out))
}
pub(super) fn value_flat(v: &Value) -> Vec<f64> {
match v {
Value::Scalar(s) => vec![*s],
Value::Array(a) => a.iter().copied().collect(),
}
}
pub(super) fn broadcast_value(v: &Value, target: &[usize]) -> ArrayD<f64> {
match v {
Value::Scalar(s) => ArrayD::<f64>::from_elem(IxDyn(target), *s),
Value::Array(a) => match pad_trailing(a, target.len()).broadcast(IxDyn(target)) {
Some(b) => b.to_owned(),
None => ArrayD::<f64>::from_elem(IxDyn(target), f64::NAN),
},
}
}
pub(super) fn combine(op: &str, a: Value, b: Value) -> Value {
match (a, b) {
(Value::Scalar(x), Value::Scalar(y)) => Value::Scalar(apply_binary(op, x, y)),
(Value::Scalar(x), Value::Array(ya)) => {
let k = binary_kernel_of(BinCode::of(op));
Value::Array(Box::new(ya.mapv(|y| k(x, y))))
}
(Value::Array(xa), Value::Scalar(y)) => {
let k = binary_kernel_of(BinCode::of(op));
Value::Array(Box::new(xa.mapv(|x| k(x, y))))
}
(Value::Array(xa), Value::Array(ya)) => {
Value::Array(Box::new(broadcast_binary(op, &xa, &ya)))
}
}
}
pub(crate) fn apply_binary(op: &str, x: f64, y: f64) -> f64 {
if crate::precision::is_f32() {
return binary_kernel_f32_of(BinCode::of(op))(x, y);
}
match op {
"+" => x + y,
"-" => x - y,
"*" => x * y,
"/" => x / y,
"^" | "pow" => x.powf(y),
"atan2" => x.atan2(y),
"min" => x.min(y),
"max" => x.max(y),
"==" | "!=" | "<" | "<=" | ">" | ">=" => scalar_compare(op, x, y),
"and" => (x != 0.0 && y != 0.0) as i32 as f64,
"or" => (x != 0.0 || y != 0.0) as i32 as f64,
_ => f64::NAN,
}
}
#[cfg(test)] pub(crate) fn binary_kernel(op: &str) -> fn(f64, f64) -> f64 {
binary_kernel_of(BinCode::of(op))
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) enum BinCode {
Add,
Sub,
Mul,
Div,
Pow,
Atan2,
Min,
Max,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
And,
Or,
Unknown,
}
impl BinCode {
pub(crate) fn of(op: &str) -> BinCode {
match op {
"+" => BinCode::Add,
"-" => BinCode::Sub,
"*" => BinCode::Mul,
"/" => BinCode::Div,
"^" | "pow" => BinCode::Pow,
"atan2" => BinCode::Atan2,
"min" => BinCode::Min,
"max" => BinCode::Max,
"==" => BinCode::Eq,
"!=" => BinCode::Ne,
"<" => BinCode::Lt,
"<=" => BinCode::Le,
">" => BinCode::Gt,
">=" => BinCode::Ge,
"and" => BinCode::And,
"or" => BinCode::Or,
_ => BinCode::Unknown,
}
}
}
pub(crate) fn binary_kernel_of(op: BinCode) -> fn(f64, f64) -> f64 {
if crate::precision::is_f32() {
return binary_kernel_f32_of(op);
}
match op {
BinCode::Add => |x, y| x + y,
BinCode::Sub => |x, y| x - y,
BinCode::Mul => |x, y| x * y,
BinCode::Div => |x, y| x / y,
BinCode::Pow => |x: f64, y: f64| x.powf(y),
BinCode::Atan2 => |x: f64, y: f64| x.atan2(y),
BinCode::Min => |x: f64, y: f64| x.min(y),
BinCode::Max => |x: f64, y: f64| x.max(y),
BinCode::Eq => |x: f64, y: f64| (x == y) as i32 as f64,
BinCode::Ne => |x: f64, y: f64| (x != y) as i32 as f64,
BinCode::Lt => |x: f64, y: f64| (x < y) as i32 as f64,
BinCode::Le => |x: f64, y: f64| (x <= y) as i32 as f64,
BinCode::Gt => |x: f64, y: f64| (x > y) as i32 as f64,
BinCode::Ge => |x: f64, y: f64| (x >= y) as i32 as f64,
BinCode::And => |x: f64, y: f64| (x != 0.0 && y != 0.0) as i32 as f64,
BinCode::Or => |x: f64, y: f64| (x != 0.0 || y != 0.0) as i32 as f64,
BinCode::Unknown => |_, _| f64::NAN,
}
}
pub(crate) fn binary_kernel_f32_of(op: BinCode) -> fn(f64, f64) -> f64 {
match op {
BinCode::Add => |x: f64, y: f64| ((x as f32) + (y as f32)) as f64,
BinCode::Sub => |x: f64, y: f64| ((x as f32) - (y as f32)) as f64,
BinCode::Mul => |x: f64, y: f64| ((x as f32) * (y as f32)) as f64,
BinCode::Div => |x: f64, y: f64| ((x as f32) / (y as f32)) as f64,
BinCode::Pow => |x: f64, y: f64| (x as f32).powf(y as f32) as f64,
BinCode::Atan2 => |x: f64, y: f64| (x as f32).atan2(y as f32) as f64,
BinCode::Min => |x: f64, y: f64| (x as f32).min(y as f32) as f64,
BinCode::Max => |x: f64, y: f64| (x as f32).max(y as f32) as f64,
BinCode::Eq => |x: f64, y: f64| ((x as f32) == (y as f32)) as i32 as f64,
BinCode::Ne => |x: f64, y: f64| ((x as f32) != (y as f32)) as i32 as f64,
BinCode::Lt => |x: f64, y: f64| ((x as f32) < (y as f32)) as i32 as f64,
BinCode::Le => |x: f64, y: f64| ((x as f32) <= (y as f32)) as i32 as f64,
BinCode::Gt => |x: f64, y: f64| ((x as f32) > (y as f32)) as i32 as f64,
BinCode::Ge => |x: f64, y: f64| ((x as f32) >= (y as f32)) as i32 as f64,
BinCode::And => |x: f64, y: f64| ((x as f32) != 0.0 && (y as f32) != 0.0) as i32 as f64,
BinCode::Or => |x: f64, y: f64| ((x as f32) != 0.0 || (y as f32) != 0.0) as i32 as f64,
BinCode::Unknown => |_, _| f64::NAN,
}
}
pub(super) fn broadcast_binary(op: &str, a: &ArrayD<f64>, b: &ArrayD<f64>) -> ArrayD<f64> {
let max_rank = a.ndim().max(b.ndim());
let a_padded = pad_trailing(a, max_rank);
let b_padded = pad_trailing(b, max_rank);
let target_shape = broadcast_shape(a_padded.shape(), b_padded.shape());
let (Some(av), Some(bv)) = (
a_padded.broadcast(IxDyn(&target_shape)),
b_padded.broadcast(IxDyn(&target_shape)),
) else {
let nan_shape: Vec<usize> = target_shape.iter().map(|&d| d.max(1)).collect();
return ArrayD::<f64>::from_elem(IxDyn(&nan_shape), f64::NAN);
};
let mut out = ArrayD::<f64>::zeros(IxDyn(&target_shape));
let k = binary_kernel_of(BinCode::of(op));
ndarray::Zip::from(&mut out)
.and(&av)
.and(&bv)
.for_each(|o, &x, &y| {
*o = k(x, y);
});
out
}
pub(super) fn broadcast_shape(a: &[usize], b: &[usize]) -> Vec<usize> {
let n = a.len().max(b.len());
let mut out = vec![1usize; n];
for i in 0..n {
let ai = if i < a.len() { a[i] } else { 1 };
let bi = if i < b.len() { b[i] } else { 1 };
let dim = if ai == bi {
ai
} else if ai == 1 {
bi
} else if bi == 1 {
ai
} else {
0
};
out[i] = dim;
}
out
}
pub(super) fn pad_trailing(arr: &ArrayD<f64>, target_rank: usize) -> ArrayD<f64> {
if arr.ndim() >= target_rank {
return arr.clone();
}
let mut shape = arr.shape().to_vec();
while shape.len() < target_rank {
shape.push(1);
}
arr.clone()
.into_shape_with_order(IxDyn(&shape))
.expect("pad_trailing reshape")
}
pub(super) fn eval_unary(op: &str, args: &[Expr], ctx: &mut EvalCtx) -> Value {
let Some(arg0) = args.first() else {
return Value::Scalar(f64::NAN);
};
let v = eval(arg0, ctx);
match v {
Value::Scalar(s) => Value::Scalar(apply_unary(op, s)),
Value::Array(a) => {
let k = unary_kernel_of(UnCode::of(op));
Value::Array(Box::new(a.mapv(k)))
}
}
}
pub(crate) fn apply_unary(op: &str, x: f64) -> f64 {
if crate::precision::is_f32() {
return unary_kernel_f32_of(UnCode::of(op))(x);
}
match op {
"exp" => x.exp(),
"log" => x.ln(),
"log10" => x.log10(),
"sqrt" => x.sqrt(),
"abs" => x.abs(),
"sign" => {
if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}
}
"floor" => x.floor(),
"ceil" => x.ceil(),
"sin" => x.sin(),
"cos" => x.cos(),
"tan" => x.tan(),
"asin" => x.asin(),
"acos" => x.acos(),
"atan" => x.atan(),
"sinh" => x.sinh(),
"cosh" => x.cosh(),
"tanh" => x.tanh(),
"asinh" => x.asinh(),
"acosh" => x.acosh(),
"atanh" => x.atanh(),
"not" => (x == 0.0) as i32 as f64,
_ => f64::NAN,
}
}
#[cfg(test)] pub(crate) fn unary_kernel(op: &str) -> fn(f64) -> f64 {
unary_kernel_of(UnCode::of(op))
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) enum UnCode {
Exp,
Ln,
Log10,
Sqrt,
Abs,
Sign,
Floor,
Ceil,
Sin,
Cos,
Tan,
Asin,
Acos,
Atan,
Sinh,
Cosh,
Tanh,
Asinh,
Acosh,
Atanh,
Not,
Unknown,
}
impl UnCode {
pub(crate) fn of(op: &str) -> UnCode {
match op {
"exp" => UnCode::Exp,
"log" => UnCode::Ln,
"log10" => UnCode::Log10,
"sqrt" => UnCode::Sqrt,
"abs" => UnCode::Abs,
"sign" => UnCode::Sign,
"floor" => UnCode::Floor,
"ceil" => UnCode::Ceil,
"sin" => UnCode::Sin,
"cos" => UnCode::Cos,
"tan" => UnCode::Tan,
"asin" => UnCode::Asin,
"acos" => UnCode::Acos,
"atan" => UnCode::Atan,
"sinh" => UnCode::Sinh,
"cosh" => UnCode::Cosh,
"tanh" => UnCode::Tanh,
"asinh" => UnCode::Asinh,
"acosh" => UnCode::Acosh,
"atanh" => UnCode::Atanh,
"not" => UnCode::Not,
_ => UnCode::Unknown,
}
}
}
pub(crate) fn unary_kernel_of(op: UnCode) -> fn(f64) -> f64 {
if crate::precision::is_f32() {
return unary_kernel_f32_of(op);
}
match op {
UnCode::Exp => |x: f64| x.exp(),
UnCode::Ln => |x: f64| x.ln(),
UnCode::Log10 => |x: f64| x.log10(),
UnCode::Sqrt => |x: f64| x.sqrt(),
UnCode::Abs => |x: f64| x.abs(),
UnCode::Sign => |x: f64| {
if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}
},
UnCode::Floor => |x: f64| x.floor(),
UnCode::Ceil => |x: f64| x.ceil(),
UnCode::Sin => |x: f64| x.sin(),
UnCode::Cos => |x: f64| x.cos(),
UnCode::Tan => |x: f64| x.tan(),
UnCode::Asin => |x: f64| x.asin(),
UnCode::Acos => |x: f64| x.acos(),
UnCode::Atan => |x: f64| x.atan(),
UnCode::Sinh => |x: f64| x.sinh(),
UnCode::Cosh => |x: f64| x.cosh(),
UnCode::Tanh => |x: f64| x.tanh(),
UnCode::Asinh => |x: f64| x.asinh(),
UnCode::Acosh => |x: f64| x.acosh(),
UnCode::Atanh => |x: f64| x.atanh(),
UnCode::Not => |x: f64| (x == 0.0) as i32 as f64,
UnCode::Unknown => |_| f64::NAN,
}
}
pub(crate) fn unary_kernel_f32_of(op: UnCode) -> fn(f64) -> f64 {
match op {
UnCode::Exp => |x: f64| (x as f32).exp() as f64,
UnCode::Ln => |x: f64| (x as f32).ln() as f64,
UnCode::Log10 => |x: f64| (x as f32).log10() as f64,
UnCode::Sqrt => |x: f64| (x as f32).sqrt() as f64,
UnCode::Abs => |x: f64| (x as f32).abs() as f64,
UnCode::Sign => |x: f64| {
let x = x as f32;
if x > 0.0 {
1.0
} else if x < 0.0 {
-1.0
} else {
0.0
}
},
UnCode::Floor => |x: f64| (x as f32).floor() as f64,
UnCode::Ceil => |x: f64| (x as f32).ceil() as f64,
UnCode::Sin => |x: f64| (x as f32).sin() as f64,
UnCode::Cos => |x: f64| (x as f32).cos() as f64,
UnCode::Tan => |x: f64| (x as f32).tan() as f64,
UnCode::Asin => |x: f64| (x as f32).asin() as f64,
UnCode::Acos => |x: f64| (x as f32).acos() as f64,
UnCode::Atan => |x: f64| (x as f32).atan() as f64,
UnCode::Sinh => |x: f64| (x as f32).sinh() as f64,
UnCode::Cosh => |x: f64| (x as f32).cosh() as f64,
UnCode::Tanh => |x: f64| (x as f32).tanh() as f64,
UnCode::Asinh => |x: f64| (x as f32).asinh() as f64,
UnCode::Acosh => |x: f64| (x as f32).acosh() as f64,
UnCode::Atanh => |x: f64| (x as f32).atanh() as f64,
UnCode::Not => |x: f64| ((x as f32) == 0.0) as i32 as f64,
UnCode::Unknown => |_| f64::NAN,
}
}
#[cfg(test)]
mod kernel_equivalence_tests {
use super::*;
use std::hint::black_box;
const XS: &[f64] = &[
0.0,
-0.0,
1.0,
-1.0,
0.5,
-3.25,
2.0,
f64::MIN_POSITIVE,
5e-324,
1e300,
f64::INFINITY,
f64::NEG_INFINITY,
f64::NAN,
];
#[rustfmt::skip]
const BIN_OPS: &[&str] = &[
"+", "-", "*", "/", "^", "atan2", "min", "max",
"==", "!=", "<", "<=", ">", ">=", "and", "or", "no_such_op",
];
#[rustfmt::skip]
const UN_OPS: &[&str] = &[
"exp", "log", "log10", "sqrt", "abs", "sign", "floor", "ceil", "sin", "cos",
"tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "asinh", "acosh", "atanh",
"not", "no_such_op",
];
#[test]
fn binary_kernels_match_apply_binary() {
for op in BIN_OPS {
let k = binary_kernel(op);
for &x in XS {
for &y in XS {
let a = apply_binary(op, x, y);
let b = k(x, y);
assert_eq!(
a.to_bits(),
b.to_bits(),
"binary_kernel(\"{op}\")({x}, {y}) = {b} != apply_binary = {a}"
);
}
}
}
}
#[test]
fn unary_kernels_match_apply_unary() {
for op in UN_OPS {
let k = unary_kernel(op);
for &x in XS {
let a = apply_unary(op, x);
let b = k(x);
assert_eq!(
a.to_bits(),
b.to_bits(),
"unary_kernel(\"{op}\")({x}) = {b} != apply_unary = {a}"
);
}
}
}
fn same_bits(a: f64, b: f64) -> bool {
(a.is_nan() && b.is_nan()) || a.to_bits() == b.to_bits()
}
#[rustfmt::skip]
const F32_XS: &[f64] = &[
0.0, -0.0, 1.0, -1.0, 0.1, 0.5, -3.25, 2.0, 3.0, 26.5, 73.5, 100.0,
1e-45, 5e-324, f64::MIN_POSITIVE, 1e38, 1e39, -1e39,
f64::INFINITY, f64::NEG_INFINITY, f64::NAN,
];
#[test]
fn binary_kernels_f32_are_binary32() {
let _g = crate::precision::enter(crate::precision::Precision::Float32);
for &x in F32_XS {
for &y in F32_XS {
let (xf, yf) = (|| black_box(x as f32), || black_box(y as f32));
#[rustfmt::skip]
let cases: &[(&str, f64)] = &[
("+", (xf() + yf()) as f64),
("-", (xf() - yf()) as f64),
("*", (xf() * yf()) as f64),
("/", (xf() / yf()) as f64),
("^", xf().powf(yf()) as f64),
("atan2", xf().atan2(yf()) as f64),
("min", xf().min(yf()) as f64),
("max", xf().max(yf()) as f64),
("==", (xf() == yf()) as i32 as f64),
("!=", (xf() != yf()) as i32 as f64),
("<", (xf() < yf()) as i32 as f64),
("<=", (xf() <= yf()) as i32 as f64),
(">", (xf() > yf()) as i32 as f64),
(">=", (xf() >= yf()) as i32 as f64),
("and", (xf() != 0.0 && yf() != 0.0) as i32 as f64),
("or", (xf() != 0.0 || yf() != 0.0) as i32 as f64),
];
for &(op, want) in cases {
let got = apply_binary(op, x, y);
assert!(
same_bits(got, want),
"apply_binary(\"{op}\", {x:?}, {y:?}) = {got:?}, want f32 {want:?}"
);
let hoisted = binary_kernel_f32_of(BinCode::of(op))(x, y);
assert!(
same_bits(hoisted, want),
"binary_kernel_f32_of({op}) disagrees with apply_binary"
);
}
}
}
}
#[test]
fn unary_kernels_f32_are_binary32() {
let _g = crate::precision::enter(crate::precision::Precision::Float32);
for &x in F32_XS {
let xf = || black_box(x as f32);
#[rustfmt::skip]
let cases: &[(&str, f64)] = &[
("exp", xf().exp() as f64),
("log", xf().ln() as f64),
("log10", xf().log10() as f64),
("sqrt", xf().sqrt() as f64),
("abs", xf().abs() as f64),
("floor", xf().floor() as f64),
("ceil", xf().ceil() as f64),
("sin", xf().sin() as f64),
("cos", xf().cos() as f64),
("tan", xf().tan() as f64),
("asin", xf().asin() as f64),
("acos", xf().acos() as f64),
("atan", xf().atan() as f64),
("sinh", xf().sinh() as f64),
("cosh", xf().cosh() as f64),
("tanh", xf().tanh() as f64),
("asinh", xf().asinh() as f64),
("acosh", xf().acosh() as f64),
("atanh", xf().atanh() as f64),
("not", (xf() == 0.0) as i32 as f64),
];
for &(op, want) in cases {
let got = apply_unary(op, x);
assert!(
same_bits(got, want),
"apply_unary(\"{op}\", {x:?}) = {got:?}, want f32 {want:?}"
);
let hoisted = unary_kernel_f32_of(UnCode::of(op))(x);
assert!(
same_bits(hoisted, want),
"unary_kernel_f32_of({op}) disagrees with apply_unary"
);
}
}
}
#[test]
fn float64_kernels_are_bit_identical_to_plain_f64() {
assert_eq!(
crate::precision::active(),
crate::precision::Precision::Float64,
"Float64 is the default and must need no guard"
);
for &x in F32_XS {
for &y in F32_XS {
#[rustfmt::skip]
let cases: &[(&str, f64)] = &[
("+", x + y), ("-", x - y), ("*", x * y), ("/", x / y),
("^", x.powf(y)), ("atan2", x.atan2(y)),
("min", x.min(y)), ("max", x.max(y)),
("==", (x == y) as i32 as f64), ("!=", (x != y) as i32 as f64),
("<", (x < y) as i32 as f64), ("<=", (x <= y) as i32 as f64),
(">", (x > y) as i32 as f64), (">=", (x >= y) as i32 as f64),
("and", (x != 0.0 && y != 0.0) as i32 as f64),
("or", (x != 0.0 || y != 0.0) as i32 as f64),
];
for &(op, want) in cases {
assert!(
same_bits(apply_binary(op, x, y), want),
"apply_binary(\"{op}\", {x:?}, {y:?}) drifted from plain f64"
);
}
}
#[rustfmt::skip]
let un: &[(&str, f64)] = &[
("exp", x.exp()), ("log", x.ln()), ("log10", x.log10()),
("sqrt", x.sqrt()), ("abs", x.abs()), ("floor", x.floor()),
("ceil", x.ceil()), ("sin", x.sin()), ("cos", x.cos()),
("tan", x.tan()), ("tanh", x.tanh()),
];
for &(op, want) in un {
assert!(
same_bits(apply_unary(op, x), want),
"apply_unary(\"{op}\", {x:?}) drifted from plain f64"
);
}
}
}
#[test]
fn the_witness_expression_rounds_per_operation() {
let expr = |h: f64, s: f64| {
let num = apply_binary("*", h, apply_binary("/", apply_binary("-", h, s), h));
apply_binary("/", num, apply_binary("-", h, s))
};
assert_eq!(expr(100.0, 73.5).to_bits(), 1.0_f64.to_bits());
let _g = crate::precision::enter(crate::precision::Precision::Float32);
assert_eq!(
expr(100.0, 73.5).to_bits(),
(0.99999994_f32 as f64).to_bits(),
"the Float32 kernels must reproduce the binary32 answer"
);
}
#[test]
fn fold_scalar_rounds_its_seed() {
let _g = crate::precision::enter(crate::precision::Precision::Float32);
assert_eq!(
fold_scalar("+", &[0.1]).to_bits(),
(0.1_f32 as f64).to_bits()
);
let want = ((0.1_f32 + 0.2_f32) + 0.3_f32) as f64;
assert_eq!(fold_scalar("+", &[0.1, 0.2, 0.3]).to_bits(), want.to_bits());
}
#[test]
fn const_literals_round_on_materialization() {
let _g = crate::precision::enter(crate::precision::Precision::Float32);
let scalar = json_to_value(&serde_json::json!(0.1)).expect("scalar const");
match scalar {
Value::Scalar(s) => assert_eq!(s.to_bits(), (0.1_f32 as f64).to_bits()),
other => panic!("expected a scalar, got {other:?}"),
}
let arr = json_to_value(&serde_json::json!([0.1, 0.2])).expect("array const");
match arr {
Value::Array(a) => {
assert_eq!(a[[0]].to_bits(), (0.1_f32 as f64).to_bits());
assert_eq!(a[[1]].to_bits(), (0.2_f32 as f64).to_bits());
}
other => panic!("expected an array, got {other:?}"),
}
}
#[test]
fn const_literal_memo_matches_the_direct_walk() {
let node = ExpressionNode {
op: "const".to_string(),
value: Some(serde_json::json!([[1.5, -2.5], [3.25, 4.75]])),
..Default::default()
};
let want = match json_to_value(node.value.as_ref().unwrap()).expect("array const") {
Value::Array(a) => *a,
other => panic!("expected an array, got {other:?}"),
};
let memo = ConstLitMemo::default();
let first = memo.get(&node).expect("memoized array");
assert_eq!(*first, want);
for _ in 0..3 {
let got = memo.get(&node).expect("memoized array");
assert_eq!(*got, want);
assert!(std::sync::Arc::ptr_eq(&first, &got));
}
match (
eval_const_memo(&node, Some(&memo)),
eval_const_memo(&node, None),
) {
(Value::Array(a), Value::Array(b)) => assert_eq!(*a, *b),
other => panic!("expected two arrays, got {other:?}"),
}
}
#[test]
fn const_literal_memo_is_keyed_by_precision() {
let node = ExpressionNode {
op: "const".to_string(),
value: Some(serde_json::json!([0.1, 0.2])),
..Default::default()
};
let memo = ConstLitMemo::default();
let f64_first = memo.get(&node).expect("binary64 array")[[0]];
let f32_seen = {
let _g = crate::precision::enter(crate::precision::Precision::Float32);
memo.get(&node).expect("binary32 array")[[0]]
};
let f64_again = memo.get(&node).expect("binary64 array again")[[0]];
assert_eq!(f64_first.to_bits(), 0.1_f64.to_bits());
assert_eq!(f32_seen.to_bits(), (0.1_f32 as f64).to_bits());
assert_eq!(f64_again.to_bits(), f64_first.to_bits());
}
#[test]
fn const_literal_memo_keeps_a_ragged_literal_unevaluable() {
let node = ExpressionNode {
op: "const".to_string(),
value: Some(serde_json::json!([[1.0, 2.0], [3.0]])),
..Default::default()
};
let memo = ConstLitMemo::default();
assert!(memo.get(&node).is_none());
for m in [Some(&memo), None] {
match eval_const_memo(&node, m) {
Value::Scalar(s) => assert!(s.is_nan()),
other => panic!("expected the NaN sentinel, got {other:?}"),
}
}
}
#[test]
fn const_literal_memo_is_dropped_when_the_rule_set_changes() {
let node = ExpressionNode {
op: "const".to_string(),
value: Some(serde_json::json!([1.0, 2.0, 3.0])),
..Default::default()
};
let memo = ConstLitMemo::default();
memo.retarget(0x1234);
let first = memo.get(&node).expect("memoized array");
assert!(std::sync::Arc::ptr_eq(
&first,
&memo.get(&node).expect("same rule set, same entry")
));
memo.retarget(0x1234);
assert!(std::sync::Arc::ptr_eq(
&first,
&memo.get(&node).expect("same key, same entry")
));
memo.retarget(0x5678);
let after = memo.get(&node).expect("rebuilt array");
assert!(!std::sync::Arc::ptr_eq(&first, &after));
assert_eq!(*after, *first);
}
}
pub(super) fn eval_binary(op: &str, args: &[Expr], ctx: &mut EvalCtx) -> Value {
let ([a_expr, b_expr] | [a_expr, b_expr, ..]) = args else {
return Value::Scalar(f64::NAN);
};
let a = eval(a_expr, ctx);
let b = eval(b_expr, ctx);
combine(op, a, b)
}
pub(super) fn lookup_array_ref<'a>(name: &str, ctx: &'a EvalCtx) -> Option<&'a ArrayD<f64>> {
if name == "t" || ctx.loop_binds.contains_key(name) {
return None;
}
if let Some(a) = ctx.state_arrays.get(name) {
return if a.ndim() == 0 { None } else { Some(a) };
}
if let Some(a) = ctx.observed_arrays.get(name) {
return if a.ndim() == 0 { None } else { Some(a) };
}
None
}
thread_local! {
static PER_CELL_WALKS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
pub(crate) fn per_cell_walks() -> u64 {
PER_CELL_WALKS.with(std::cell::Cell::get)
}
fn note_per_cell_walk() {
PER_CELL_WALKS.with(|c| c.set(c.get().wrapping_add(1)));
}
#[cfg(test)]
thread_local! {
static PER_CELL_CELLS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn per_cell_cells() -> u64 {
PER_CELL_CELLS.with(std::cell::Cell::get)
}
#[inline]
pub(super) fn note_per_cell_cell() {
#[cfg(test)]
PER_CELL_CELLS.with(|c| c.set(c.get() + 1));
}
#[derive(Clone, Copy, Default)]
struct FirstCellStop {
armed: bool,
stopped: bool,
with_error: bool,
}
thread_local! {
static FIRST_CELL_STOP: std::cell::Cell<FirstCellStop> = const {
std::cell::Cell::new(FirstCellStop { armed: false, stopped: false, with_error: false })
};
}
pub(crate) struct StopAtFirstCell(FirstCellStop);
impl StopAtFirstCell {
pub(crate) fn arm() -> Self {
StopAtFirstCell(FIRST_CELL_STOP.with(|c| {
c.replace(FirstCellStop {
armed: true,
..FirstCellStop::default()
})
}))
}
pub(crate) fn stopped(&self) -> bool {
FIRST_CELL_STOP.with(|c| c.get().stopped)
}
}
impl Drop for StopAtFirstCell {
fn drop(&mut self) {
FIRST_CELL_STOP.with(|c| c.set(self.0));
}
}
pub(crate) const ONE_CELL_NOTE: &str = "only one cell of it was evaluated before this refusal, \
so a document error in a cell that was not (an out-of-range gather at the last cell, say) \
is not reported here; the interpreter evaluates every cell and reports it";
pub(crate) fn per_cell_walk_refused() -> bool {
let s = FIRST_CELL_STOP.with(std::cell::Cell::get);
s.stopped && !s.with_error
}
fn per_cell_walk_stopped() -> bool {
FIRST_CELL_STOP.with(|c| c.get().stopped)
}
fn stop_at_first_cell() -> bool {
FIRST_CELL_STOP.with(|c| c.get().armed)
}
pub(super) fn stop_after_first_cell() -> bool {
FIRST_CELL_STOP.with(|c| {
let mut s = c.get();
if !s.armed {
return false;
}
if !s.stopped {
s.stopped = true;
s.with_error = CONST_OOB.with(|l| l.borrow().is_some());
c.set(s);
}
true
})
}
thread_local! {
static CONST_OOB: RefCell<Option<LatchedFault>> = const { RefCell::new(None) };
}
struct LatchedFault {
msg: String,
order: Option<(u32, SmallVec<[i64; 12]>)>,
}
pub fn take_const_array_oob() -> Option<String> {
CONST_OOB.with(|c| c.borrow_mut().take().map(|l| l.msg))
}
pub(crate) struct FaultScope {
prev: Option<LatchedFault>,
}
impl FaultScope {
pub(crate) fn enter() -> FaultScope {
FaultScope {
prev: CONST_OOB.with(|c| c.borrow_mut().take()),
}
}
pub(crate) fn finish(mut self) -> Option<String> {
let prev = self.prev.take();
CONST_OOB.with(|c| std::mem::replace(&mut *c.borrow_mut(), prev).map(|l| l.msg))
}
}
impl Drop for FaultScope {
fn drop(&mut self) {
if let Some(prev) = self.prev.take() {
CONST_OOB.with(|c| *c.borrow_mut() = Some(prev));
}
}
}
pub(crate) fn latch_gather_fault(msg: String) {
CONST_OOB.with(|c| {
let mut slot = c.borrow_mut();
if slot.is_none() {
*slot = Some(LatchedFault { msg, order: None });
}
});
}
pub(crate) fn latch_ordered_fault(rule: u32, key: &[i64], msg: impl FnOnce() -> String) {
CONST_OOB.with(|c| {
let mut slot = c.borrow_mut();
let replace = match &*slot {
None => true,
Some(LatchedFault {
order: Some((r, k)),
..
}) => *r == rule && key < &k[..],
Some(_) => false,
};
if replace {
*slot = Some(LatchedFault {
msg: msg(),
order: Some((rule, key.iter().copied().collect())),
});
}
});
}
fn latch_recur_unavailable(name: &str, raw: &[i64]) {
latch_gather_fault(recur_unavailable_message(name, raw));
}
pub(crate) fn recur_unavailable_message(name: &str, raw: &[i64]) -> String {
let at = raw
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(",");
format!(
"E_TREEWALK_RECUR_UNAVAILABLE: causal self-read of '{name}' at cell [{at}] is not \
available — the position is outside the recurrence axis, or the sweep has not \
published that cell yet (esm-spec §4.3.1.1; CONFORMANCE_SPEC.md §5.19.4: a causal \
self-read is fail-closed, never the §5.5.5 zero ghost and never a NaN a `max(x, 0)` \
could launder). Guard the base case inside the body, e.g. \
`ifelse(k <= 1, <base>, <recurrence>)`."
)
}
fn latch_index_on_scalar(base: &Expr, subscripts: usize) {
latch_gather_fault(index_on_scalar_message(base, subscripts));
}
pub(crate) fn index_on_scalar_message(base: &Expr, subscripts: usize) -> String {
let what = match base {
Expr::Variable(name) => format!("'{name}'"),
Expr::Operator(node) => format!("the `{}` result", node.op),
Expr::Integer(_) | Expr::Number(_) => "a numeric literal".to_string(),
};
format!(
"E_TREEWALK_INDEX_ON_SCALAR: {what} has no axes, so the {subscripts} subscript(s) \
applied to it name nothing (esm-spec §4.3.4; CONFORMANCE_SPEC.md §7.1). Fail-closed: \
never the §5.5.5 zero ghost, which is the boundary convention for a gather that HAS \
an axis to fall outside of, and never a bare NaN. Read an unshaped quantity by its \
bare name or as `index(<name>)` with no subscript, or give it a `shape` if it was \
meant to have axes."
)
}
fn latch_const_oob(name: &str, one_based: i64, n: i64, d: usize) {
latch_gather_fault(const_oob_message(name, one_based, n, d));
}
pub(crate) fn const_oob_message(name: &str, one_based: i64, n: i64, d: usize) -> String {
format!(
"E_TREEWALK_CONSTARRAY_OOB: const array '{name}' index {one_based} out of range \
1..{n} in dim {d} (CONFORMANCE_SPEC.md §5.5.5: the zero-ghost convention is \
never applied to a const-array gather; declare a per-dimension boundary policy \
to resolve it as `periodic` or `clamp`)"
)
}
#[derive(Clone, Copy)]
pub(super) enum GatherKind<'a> {
ZeroGhost,
ConstArray {
name: &'a str,
scope: &'a ConstArrayScope,
},
}
pub(super) fn index_into(
arr: &ArrayD<f64>,
raw: &[i64],
mut in_bounds: bool,
kind: GatherKind<'_>,
) -> Value {
let mut indices: DimU = SmallVec::with_capacity(raw.len());
for (d, &one_based) in raw.iter().enumerate() {
let dim_size = arr.shape().get(d).copied().unwrap_or(0) as i64;
let mut resolved = one_based;
if one_based < 1 || one_based > dim_size {
match kind {
GatherKind::ZeroGhost => in_bounds = false,
GatherKind::ConstArray { name, scope } => {
match (dim_size >= 1).then(|| scope.boundary(name, d)) {
Some(BoundaryKind::Periodic) => {
resolved = (one_based - 1).rem_euclid(dim_size) + 1;
}
Some(BoundaryKind::Clamp) => {
resolved = one_based.clamp(1, dim_size);
}
Some(BoundaryKind::Error) | None => {
latch_const_oob(name, one_based, dim_size, d);
return Value::Scalar(f64::NAN);
}
}
}
}
}
indices.push((resolved - 1).max(0) as usize);
}
if !in_bounds {
return Value::Scalar(0.0);
}
if indices.len() > arr.ndim() {
return Value::Scalar(f64::NAN);
}
if indices.len() < arr.ndim() {
let mut view = arr.view();
for &ix in &indices {
view = view.index_axis_move(ndarray::Axis(0), ix);
}
return Value::Array(Box::new(view.to_owned()));
}
match arr.get(IxDyn(&indices)) {
Some(v) => Value::Scalar(*v),
None => Value::Scalar(0.0),
}
}
#[inline]
fn eval_index_args(args: &[Expr], ctx: &mut EvalCtx) -> (DimI, bool) {
let mut raw: DimI = SmallVec::with_capacity(args.len());
let mut in_bounds = true;
for a in args {
match eval(a, ctx).as_scalar() {
Some(f) => raw.push(f.round() as i64),
None => {
in_bounds = false;
raw.push(0);
}
}
}
(raw, in_bounds)
}
pub(super) fn eval_index(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
if node.args.is_empty() {
return Value::Scalar(f64::NAN);
}
if node.args.is_empty() {
return Value::Scalar(f64::NAN);
}
if let Some(scope) = ctx.recur
&& matches!(&node.args[0], Expr::Variable(name) if name == scope.name)
{
let (raw, in_bounds) = eval_index_args(&node.args[1..], ctx);
if !in_bounds {
latch_recur_unavailable(scope.name, &raw);
return Value::Scalar(f64::NAN);
}
return match scope.read(&raw) {
Some(v) => Value::Scalar(crate::precision::active().round(v)),
None => {
latch_recur_unavailable(scope.name, &raw);
Value::Scalar(f64::NAN)
}
};
}
if let Expr::Variable(name) = &node.args[0]
&& lookup_array_ref(name, ctx).is_some()
{
let (raw, in_bounds) = eval_index_args(&node.args[1..], ctx);
let kind = gather_kind(name, ctx);
if let Some(arr) = lookup_array_ref(name, ctx) {
return index_into(arr, &raw, in_bounds, kind);
}
}
if let Expr::Operator(lit) = &node.args[0]
&& lit.op == "const"
&& is_inline_const_array(lit)
&& let Some(arr) = const_lit_array(lit, ctx.const_lits)
{
let (raw, in_bounds) = eval_index_args(&node.args[1..], ctx);
return index_into(
&arr,
&raw,
in_bounds,
GatherKind::ConstArray {
name: INLINE_CONST_NAME,
scope: ctx.const_arrays,
},
);
}
let const_kind = match &node.args[0] {
Expr::Variable(name) => gather_kind(name, ctx),
base if ConstArrayScope::is_inline_const(base) => GatherKind::ConstArray {
name: INLINE_CONST_NAME,
scope: ctx.const_arrays,
},
_ => GatherKind::ZeroGhost,
};
let array_val = eval(&node.args[0], ctx);
let arr = match array_val {
Value::Array(a) => a,
Value::Scalar(s) if node.args.len() == 1 => return Value::Scalar(s),
Value::Scalar(_) => {
latch_index_on_scalar(&node.args[0], node.args.len() - 1);
return Value::Scalar(f64::NAN);
}
};
let (raw, in_bounds) = eval_index_args(&node.args[1..], ctx);
index_into(&arr, &raw, in_bounds, const_kind)
}
fn gather_kind<'a>(name: &'a str, ctx: &EvalCtx<'a>) -> GatherKind<'a> {
if ctx.const_arrays.is_const(name) {
GatherKind::ConstArray {
name,
scope: ctx.const_arrays,
}
} else {
GatherKind::ZeroGhost
}
}
pub(super) fn eval_const(node: &ExpressionNode) -> Value {
node.value
.as_ref()
.and_then(json_to_value)
.unwrap_or(Value::Scalar(f64::NAN))
}
pub(super) fn eval_const_memo(node: &ExpressionNode, memo: Option<&ConstLitMemo>) -> Value {
if let Some(memo) = memo
&& is_inline_const_array(node)
&& let Some(arr) = memo.get(node)
{
return Value::Array(Box::new((*arr).clone()));
}
eval_const(node)
}
#[inline]
pub(super) fn is_inline_const_array(node: &ExpressionNode) -> bool {
matches!(node.value.as_ref(), Some(serde_json::Value::Array(_)))
}
pub(super) fn json_to_value(v: &serde_json::Value) -> Option<Value> {
use serde_json::Value as J;
let prec = crate::precision::active();
match v {
J::Number(n) => Some(Value::Scalar(prec.round(n.as_f64()?))),
J::Array(_) => {
let mut shape: Vec<usize> = Vec::new();
let mut flat: Vec<f64> = Vec::new();
collect_json_array(v, 0, &mut shape, &mut flat)?;
if prec.is_f32() {
for x in &mut flat {
*x = prec.round(*x);
}
}
ArrayD::from_shape_vec(IxDyn(&shape), flat)
.ok()
.map(|a| Value::Array(Box::new(a)))
}
_ => None,
}
}
pub(super) fn collect_json_array(
v: &serde_json::Value,
depth: usize,
shape: &mut Vec<usize>,
flat: &mut Vec<f64>,
) -> Option<()> {
use serde_json::Value as J;
match v {
J::Array(items) => {
if depth == shape.len() {
shape.push(items.len());
} else if shape[depth] != items.len() {
return None; }
for item in items {
collect_json_array(item, depth + 1, shape, flat)?;
}
Some(())
}
J::Number(n) => {
flat.push(n.as_f64()?);
Some(())
}
_ => None,
}
}
pub(super) fn eval_clip_operands(
node: &ExpressionNode,
ctx: &mut EvalCtx,
) -> Option<(crate::geometry::Manifold, Vec<(f64, f64)>, Vec<(f64, f64)>)> {
if node.args.len() != 2 {
return None;
}
let manifold = node
.manifold
.as_deref()
.and_then(crate::geometry::Manifold::from_flag)?;
let ([a_expr, b_expr] | [a_expr, b_expr, ..]) = node.args.as_slice() else {
return None;
};
let poly_a = match eval(a_expr, ctx) {
Value::Array(a) => a,
_ => return None,
};
let poly_b = match eval(b_expr, ctx) {
Value::Array(a) => a,
_ => return None,
};
let va = arrayd_to_lonlat(&poly_a)?;
let vb = arrayd_to_lonlat(&poly_b)?;
Some((manifold, va, vb))
}
pub(super) fn eval_intersect_polygon(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
let Some((manifold, va, vb)) = eval_clip_operands(node, ctx) else {
return Value::Scalar(f64::NAN);
};
match crate::geometry::intersect_polygon(&va, &vb, manifold) {
Ok(ring) => {
let closed = close_ring(&ring);
let arr = lonlat_to_arrayd(&closed);
if let Some(id) = &node.id {
ctx.derived_rings
.borrow_mut()
.insert(id.clone(), arr.clone());
}
Value::Array(Box::new(arr))
}
Err(e) => {
latch_degenerate_operand(&e);
Value::Scalar(f64::NAN)
}
}
}
pub(crate) fn latch_degenerate_operand(e: &crate::geometry::GeometryError) {
if e.is_degenerate_operand() {
latch_gather_fault(format!(
"{GEOMETRY_CLIP_CODE}: {} (esm-spec §8.6.1: a polygon operand needs at least 3 \
distinct vertices)",
e.message()
));
}
}
pub(crate) const GEOMETRY_CLIP_CODE: &str = "E_TREEWALK_GEOMETRY_CLIP";
pub(super) fn eval_polygon_intersection_area(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
let Some((manifold, va, vb)) = eval_clip_operands(node, ctx) else {
return Value::Scalar(f64::NAN);
};
Value::Scalar(clip_area_value(&va, &vb, manifold))
}
pub(crate) fn clip_area_value(
va: &[(f64, f64)],
vb: &[(f64, f64)],
manifold: crate::geometry::Manifold,
) -> f64 {
crate::geometry::intersect_polygon(va, vb, manifold)
.and_then(|ring| crate::geometry::polygon_area(&ring, manifold))
.unwrap_or_else(|e| {
latch_degenerate_operand(&e);
f64::NAN
})
}
pub(super) fn close_ring(ring: &[(f64, f64)]) -> Vec<(f64, f64)> {
if ring.is_empty() {
return Vec::new();
}
let mut out = Vec::with_capacity(ring.len() + 1);
out.extend_from_slice(ring);
out.push(ring[0]);
out
}
pub(super) fn arrayd_to_lonlat(arr: &ArrayD<f64>) -> Option<Vec<(f64, f64)>> {
if arr.ndim() != 2 || arr.shape()[1] != 2 {
return None;
}
let nv = arr.shape()[0];
let mut out = Vec::with_capacity(nv);
for v in 0..nv {
out.push((arr[IxDyn(&[v, 0])], arr[IxDyn(&[v, 1])]));
}
Some(out)
}
pub(super) fn lonlat_to_arrayd(ring: &[(f64, f64)]) -> ArrayD<f64> {
let n = ring.len();
let mut flat = Vec::with_capacity(n * 2);
for &(lon, lat) in ring {
flat.push(lon);
flat.push(lat);
}
ArrayD::from_shape_vec(IxDyn(&[n, 2]), flat).expect("ring [N,2] shape is consistent")
}
pub fn eval_expression(
expr: &Expr,
inputs: &HashMap<String, ArrayD<f64>>,
params: &[f64],
param_names: &[String],
t: f64,
) -> Result<Value, CompileError> {
eval_expression_with_extents(
expr,
inputs,
params,
param_names,
t,
crate::faq::empty_derived_extents(),
)
}
pub fn eval_expression_with_extents(
expr: &Expr,
inputs: &HashMap<String, ArrayD<f64>>,
params: &[f64],
param_names: &[String],
t: f64,
derived_extents: &HashMap<String, i64>,
) -> Result<Value, CompileError> {
eval_expression_with_extents_and_consts(
expr,
inputs,
params,
param_names,
t,
derived_extents,
ConstArrayScope::empty(),
)
}
pub fn eval_expression_with_extents_and_consts(
expr: &Expr,
inputs: &HashMap<String, ArrayD<f64>>,
params: &[f64],
param_names: &[String],
t: f64,
derived_extents: &HashMap<String, i64>,
const_arrays: &ConstArrayScope,
) -> Result<Value, CompileError> {
let inputs: ArrMap = inputs.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
eval_expression_with_extents_and_consts_shared(
expr,
&inputs,
params,
param_names,
t,
derived_extents,
const_arrays,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn eval_observed_recurrence(
name: &str,
expr: &Expr,
inputs: &ArrMap,
params: &[f64],
param_names: &[String],
t: f64,
derived_extents: &HashMap<String, i64>,
const_arrays: &ConstArrayScope,
) -> Option<Result<ArrayD<f64>, CompileError>> {
if !expr_reads_self(expr, name) {
return None;
}
let lhs = Expr::Variable(name.to_string());
let lowered = match super::compile::lower_recurrence(name, &lhs, expr) {
Ok(Some(l)) => l,
Ok(None) => {
return Some(Err(CompileError::InterpreterBuildError {
details: format!(
"recurrence_unsupported_form: '{name}' reads itself through `index` in its \
own definition, but the build pipeline could not resolve a cell frame to \
sweep. Evaluating it wholesale would resolve the self-read to an unbound \
name, and a body containing `max(x, 0)` would turn the resulting NaN into \
a plausible zero (esm-spec §4.3.1.1; CONFORMANCE_SPEC §5.19.4)."
),
}));
}
Err(e) => return Some(Err(e)),
};
let empty: ArrMap = ArrMap::default();
let derived_rings: RefCell<HashMap<String, ArrayD<f64>>> = RefCell::new(HashMap::new());
let forcing: RefCell<HashMap<String, ArrayD<f64>>> = RefCell::new(HashMap::new());
let env = EvalEnv {
state_arrays: &empty,
params,
param_names,
t,
derived_rings: &derived_rings,
derived_extents,
forcing: &forcing,
cse: None,
const_lits: None,
const_arrays,
declared: empty_declared_names(),
};
let (shape, origin) = super::rhs::recur_frame(&lowered.ranges);
let scope = RecurScope::new(name, shape, origin);
take_const_array_oob(); let arr = {
let mut ctx = env.ctx(inputs);
super::rhs::sweep_recurrence(
&scope,
&lowered.idx_names,
&lowered.ranges,
&lowered.body,
lowered.axis,
&mut ctx,
)
};
match take_const_array_oob() {
Some(details) => Some(Err(CompileError::InterpreterBuildError { details })),
None => Some(Ok(arr)),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn eval_observed_taped(
name: &str,
expr: &Expr,
inputs: &ArrMap,
params: &[f64],
param_names: &[String],
const_arrays: &ConstArrayScope,
recurrence_only: bool,
) -> Option<Result<ArrayD<f64>, super::tape::StandaloneError>> {
use super::tape::StandaloneError;
let recurrence = expr_reads_self(expr, name);
if recurrence_only && !recurrence {
return None;
}
let rule = if recurrence {
let lhs = Expr::Variable(name.to_string());
match super::compile::lower_recurrence(name, &lhs, expr) {
Ok(Some(r)) => AlgebraicRule::Recurrence {
var: name.to_string(),
output_idx_names: r.idx_names,
output_ranges: r.ranges,
body: std::sync::Arc::new(r.body),
axis: r.axis,
max_lag: r.max_lag,
lag_proven: r.lag_proven,
},
Ok(None) | Err(_) if recurrence_only => return None,
Ok(None) | Err(_) => {
return Some(Err(StandaloneError::Declined(
"recurrence: a self-read the recurrence lowering does not recognize".into(),
)));
}
}
} else {
AlgebraicRule::Scalar {
var: name.to_string(),
body: std::sync::Arc::new(expr.clone()),
declared_shape: None,
}
};
let mut mentioned = HashSet::new();
super::tape::collect_variable_names(expr, &mut mentioned);
let mut seeded: Vec<(&str, &ArrayD<f64>)> = inputs
.iter()
.filter(|(k, _)| k.as_str() != name && mentioned.contains(k.as_str()))
.map(|(k, a)| (k.as_str(), a))
.collect();
seeded.sort_by(|a, b| a.0.cmp(b.0));
Some(super::tape::eval_standalone_rule(
&rule,
&seeded,
param_names,
params,
const_arrays,
true,
))
}
fn expr_reads_self(expr: &Expr, name: &str) -> bool {
let Expr::Operator(node) = expr else {
return false;
};
if node.op == "index" && matches!(node.args.first(), Some(Expr::Variable(v)) if v == name) {
return true;
}
node.args.iter().any(|a| expr_reads_self(a, name))
|| [
node.expr.as_deref(),
node.filter.as_deref(),
node.key.as_deref(),
node.lower.as_deref(),
node.upper.as_deref(),
]
.into_iter()
.flatten()
.any(|c| expr_reads_self(c, name))
|| node
.values
.as_ref()
.is_some_and(|vs| vs.iter().any(|v| expr_reads_self(v, name)))
}
pub(crate) fn eval_expression_with_extents_and_consts_shared(
expr: &Expr,
inputs: &ArrMap,
params: &[f64],
param_names: &[String],
t: f64,
derived_extents: &HashMap<String, i64>,
const_arrays: &ConstArrayScope,
) -> Result<Value, CompileError> {
check_evaluable(expr)?;
let mut geometry_ids = std::collections::HashSet::new();
super::compile::collect_geometry_producer_ids(expr, &mut geometry_ids);
if let Some(from_faq) =
super::compile::first_unmaterialized_derived_range(expr, derived_extents, &geometry_ids)
{
return Err(super::compile::unmaterialized_derived_error(
&from_faq,
&HashMap::new(),
None,
));
}
let empty: ArrMap = ArrMap::default();
let derived_rings: RefCell<HashMap<String, ArrayD<f64>>> = RefCell::new(HashMap::new());
let forcing: RefCell<HashMap<String, ArrayD<f64>>> = RefCell::new(HashMap::new());
let env = EvalEnv {
state_arrays: &empty,
params,
param_names,
t,
derived_rings: &derived_rings,
derived_extents,
forcing: &forcing,
cse: None,
const_lits: None,
const_arrays,
declared: empty_declared_names(),
};
let mut ctx = env.ctx(inputs);
take_const_array_oob(); let v = eval(expr, &mut ctx);
match take_const_array_oob() {
Some(details) => Err(CompileError::InterpreterBuildError { details }),
None => Ok(v),
}
}
pub(super) struct CellBox<'a> {
pub names: &'a [String],
pub origin: &'a [i64],
}
pub(super) struct ReduceSpec<'a> {
pub(super) contract_names: &'a [String],
pub(super) body: &'a Expr,
pub(super) reduce: ReduceKind,
pub(super) filter: Option<&'a Expr>,
pub(super) cell: Option<&'a CellBox<'a>>,
}
pub(super) fn filter_excludes(
filter: Option<&Expr>,
cell: Option<&CellBox>,
ctx: &mut EvalCtx,
) -> bool {
let Some(f) = filter else {
return false;
};
match eval(f, ctx) {
Value::Scalar(s) => s == 0.0,
Value::Array(a) => {
if a.ndim() == 0 {
return a.first().copied().unwrap_or(0.0) == 0.0;
}
let Some(cell) = cell else {
return false;
};
if a.ndim() != cell.names.len() || cell.origin.len() != cell.names.len() {
return false;
}
let ix: Vec<usize> = cell
.names
.iter()
.zip(cell.origin.iter())
.map(|(n, &lo)| {
let bound = ctx.loop_binds.get(n).copied().unwrap_or(lo);
(bound - lo).max(0) as usize
})
.collect();
match a.get(IxDyn(&ix)) {
Some(&m) => m == 0.0,
None => false,
}
}
}
}
pub(super) fn reduce_contraction(
spec: &ReduceSpec,
contract_dims: &[ContractDim],
static_ranges: Option<&[(i64, i64)]>,
ctx: &mut EvalCtx,
) -> f64 {
let &ReduceSpec {
contract_names,
body,
reduce,
filter,
cell,
} = spec;
if contract_names.is_empty() {
return if filter_excludes(filter, cell, ctx) {
reduce.identity()
} else {
eval(body, ctx).as_scalar().unwrap_or(f64::NAN)
};
}
let derived: SmallVec<[(i64, i64); 4]>;
let ranges: &[(i64, i64)] = match static_ranges {
Some(r) => r,
None => {
derived = contract_dims.iter().map(|d| d.concrete(ctx)).collect();
&derived
}
};
let mut acc: f64 = reduce.identity();
if let (Some(cell), [j_name], [(lo, hi)]) = (cell, contract_names, ranges)
&& folds_from_the_top(cell.names, contract_names, filter)
{
for j in (*lo..=*hi).rev() {
set_bind(&mut ctx.loop_binds, j_name, j);
if filter_excludes(filter, Some(cell), ctx) {
continue;
}
let term = eval(body, ctx).as_scalar().unwrap_or(f64::NAN);
acc = reduce.combine(acc, term);
}
return acc;
}
let mut tuples = CartesianTuples::new(ranges);
while let Some(k_tuple) = tuples.next() {
for (kn, kv) in contract_names.iter().zip(k_tuple.iter()) {
set_bind(&mut ctx.loop_binds, kn, *kv);
}
if filter_excludes(filter, cell, ctx) {
continue;
}
let term = eval(body, ctx).as_scalar().unwrap_or(f64::NAN);
acc = reduce.combine(acc, term);
}
acc
}
pub(super) struct JoinGate {
sym_src: String,
sym_tgt: String,
index: Rc<crate::broad_phase::OverlapIndex>,
n_src: usize,
n_tgt: usize,
clause_ix: usize,
}
impl JoinGate {
fn selectivity_cmp(&self, other: &JoinGate) -> std::cmp::Ordering {
let (a1, s1) = (self.index.len() as i128, (self.n_src * self.n_tgt) as i128);
let (a2, s2) = (
other.index.len() as i128,
(other.n_src * other.n_tgt) as i128,
);
(a1 * s2)
.cmp(&(a2 * s1))
.then_with(|| self.clause_ix.cmp(&other.clause_ix))
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum GateAxis {
Contracted(usize),
Output,
Absent,
}
pub(super) struct GatePlacement {
src: GateAxis,
tgt: GateAxis,
}
pub(super) fn gate_axis(sym: &str, idx_names: &[String], contract_names: &[String]) -> GateAxis {
if let Some(d) = contract_names.iter().position(|n| n == sym) {
return GateAxis::Contracted(d);
}
if idx_names.iter().any(|n| n == sym) {
return GateAxis::Output;
}
GateAxis::Absent
}
pub(super) fn gate_placement(
gate: &JoinGate,
idx_names: &[String],
contract_names: &[String],
) -> GatePlacement {
GatePlacement {
src: gate_axis(&gate.sym_src, idx_names, contract_names),
tgt: gate_axis(&gate.sym_tgt, idx_names, contract_names),
}
}
trait Retained {
fn retained_pairs(&self) -> usize;
fn in_use(&self) -> bool;
}
impl Retained for Option<Rc<crate::broad_phase::OverlapIndex>> {
fn retained_pairs(&self) -> usize {
self.as_ref().map_or(0, |ix| ix.resident_pairs())
}
fn in_use(&self) -> bool {
self.as_ref().is_some_and(|ix| Rc::strong_count(ix) > 1)
}
}
impl Retained for Option<EqGateEntry> {
fn retained_pairs(&self) -> usize {
self.as_ref().map_or(0, |(ix, _, _)| ix.resident_pairs())
}
fn in_use(&self) -> bool {
self.as_ref()
.is_some_and(|(ix, _, _)| Rc::strong_count(ix) > 1)
}
}
struct CacheEntry<V> {
value: V,
pairs: usize,
used: u64,
}
struct GateCache<K, V> {
entries: HashMap<K, CacheEntry<V>>,
clock: u64,
pairs: usize,
}
const GATE_CACHE_MAX_ENTRIES: usize = 512;
impl<K: std::hash::Hash + Eq + Clone, V: Clone + Retained> GateCache<K, V> {
fn new() -> Self {
GateCache {
entries: HashMap::new(),
clock: 0,
pairs: 0,
}
}
fn get(&mut self, key: &K) -> Option<V> {
self.clock += 1;
let clock = self.clock;
let e = self.entries.get_mut(key)?;
e.used = clock;
Some(e.value.clone())
}
fn get_within_budget(&mut self, key: &K, budget: usize) -> Option<V> {
self.reprice();
self.evict_to(budget, GATE_CACHE_MAX_ENTRIES);
self.get(key)
}
fn reprice(&mut self) {
let mut total = 0;
for e in self.entries.values_mut() {
e.pairs = e.value.retained_pairs();
total += e.pairs;
}
self.pairs = total;
}
fn evict_to(&mut self, budget: usize, max_entries: usize) {
while self.pairs > budget || self.entries.len() > max_entries {
let victim = self
.entries
.iter()
.filter(|(_, e)| !e.value.in_use())
.min_by_key(|(_, e)| e.used)
.map(|(k, _)| k.clone());
let Some(victim) = victim else { return }; if let Some(e) = self.entries.remove(&victim) {
self.pairs -= e.pairs;
}
}
}
fn evict_to_budget(&mut self, budget: usize) {
self.evict_to(budget, GATE_CACHE_MAX_ENTRIES.saturating_sub(1));
}
fn insert(&mut self, key: K, value: V) {
self.clock += 1;
let pairs = value.retained_pairs();
let prev = self.entries.insert(
key,
CacheEntry {
value,
pairs,
used: self.clock,
},
);
self.pairs = self.pairs - prev.map_or(0, |e| e.pairs) + pairs;
}
}
type GateKey = (Vec<String>, Vec<String>, u64, usize, usize);
thread_local! {
static GATE_CACHE: RefCell<GateCache<GateKey, Option<Rc<crate::broad_phase::OverlapIndex>>>> =
RefCell::new(GateCache::new());
}
fn env_factor_len(name: &str, ctx: &EvalCtx) -> Option<usize> {
if let Some(a) = ctx.state_arrays.get(name) {
return Some(a.len());
}
if let Some(a) = ctx.observed_arrays.get(name) {
return Some(a.len());
}
ctx.forcing.borrow().get(name).map(|a| a.len())
}
pub(super) fn resolve_join_gates(join: &[JoinClause], ctx: &EvalCtx) -> Vec<JoinGate> {
if !crate::broad_phase::join_gate_enabled() {
return Vec::new();
}
let mut gates: Vec<JoinGate> = Vec::new();
let mut reach: HashMap<String, usize> = HashMap::new();
for (clause_ix, clause) in join.iter().enumerate() {
let Some(ov) = &clause.overlap else {
continue;
};
let (Some(sym_src), Some(sym_tgt)) = (&ov.sym_src, &ov.sym_tgt) else {
continue;
};
let eps = ov.eps.unwrap_or(0.0);
let (Some(first_src), Some(first_tgt)) = (ov.src_env.first(), ov.tgt_env.first()) else {
continue;
};
let (Some(nsrc), Some(ntgt)) = (
env_factor_len(first_src, ctx),
env_factor_len(first_tgt, ctx),
) else {
continue;
};
let key: GateKey = (
ov.src_env.clone(),
ov.tgt_env.clone(),
eps.to_bits(),
nsrc,
ntgt,
);
let budget = crate::broad_phase::gate_cache_pair_budget();
let cached = GATE_CACHE.with(|c| c.borrow_mut().get_within_budget(&key, budget));
let index = match cached {
Some(hit) => hit,
None => {
GATE_CACHE.with(|c| c.borrow_mut().evict_to_budget(budget));
crate::broad_phase::bump_gate_index_builds();
let built = build_overlap_index(&ov.src_env, &ov.tgt_env, eps, ctx).map(Rc::new);
GATE_CACHE.with(|c| c.borrow_mut().insert(key, built.clone()));
built
}
};
if let Some(index) = index {
narrow_reach(&mut reach, sym_src, index.len());
narrow_reach(&mut reach, sym_tgt, index.len());
gates.push(JoinGate {
sym_src: sym_src.clone(),
sym_tgt: sym_tgt.clone(),
index,
n_src: nsrc,
n_tgt: ntgt,
clause_ix,
});
}
}
let mut priced: Vec<EqCandidate<'_>> = Vec::new();
let mut cheapest: Option<(usize, EqSides)> = None;
let budget = crate::broad_phase::gate_cache_pair_budget();
for (clause_ix, clause) in join.iter().enumerate() {
if clause.overlap.is_some() {
continue;
}
let Some(g) = &clause.on_gate else {
continue;
};
if g.cols_l.len() != g.cols_r.len() || g.cols_l.is_empty() {
continue;
}
let Some(key) = equality_cache_key(g, ctx) else {
continue;
};
if let Some(hit) = EQ_GATE_CACHE.with(|c| c.borrow_mut().get_within_budget(&key, budget)) {
if let Some((index, n_l, n_r)) = hit {
priced.push(EqCandidate {
g,
clause_ix,
matches: index.len(),
n_l,
n_r,
key,
ready: Some((index, n_l, n_r)),
sides: None,
});
}
continue;
}
if let Some(&(n_l, n_r, matches)) = EQ_PRICE_CACHE
.with(|c| c.borrow().get(&key).copied())
.as_ref()
{
priced.push(EqCandidate {
g,
clause_ix,
matches,
n_l,
n_r,
key,
ready: None,
sides: None,
});
continue;
}
let Some(sides) = equality_sides(g, ctx) else {
EQ_GATE_CACHE.with(|c| c.borrow_mut().insert(key, None));
continue;
};
let matches = crate::relational::equijoin_match_count(&sides.1, &sides.3);
let (n_l, n_r) = (sides.0.len(), sides.2.len());
remember_price(key.clone(), (n_l, n_r, matches));
if cheapest
.as_ref()
.is_none_or(|&(i, _)| matches < priced[i].matches)
{
cheapest = Some((priced.len(), sides));
}
priced.push(EqCandidate {
g,
clause_ix,
matches,
n_l,
n_r,
key,
ready: None,
sides: None,
});
}
if let Some((slot, sides)) = cheapest {
priced[slot].sides = Some(sides);
}
priced.sort_by_key(|c| (c.matches, c.clause_ix));
let ratio = crate::broad_phase::gate_plan_ratio();
let floor = crate::broad_phase::gate_plan_floor();
for c in priced {
let space = (reach_of(&reach, &c.g.sym_l, c.n_l) as u128)
.saturating_mul(reach_of(&reach, &c.g.sym_r, c.n_r) as u128);
let (index, n_src, n_tgt) = match c.ready {
Some(entry) => entry,
None => {
if !gates.is_empty()
&& c.matches >= floor
&& (c.matches as u128) > space.saturating_mul(ratio)
{
crate::broad_phase::bump_gate_plan_declines();
continue;
}
let Some((pos_l, keys_l, pos_r, keys_r)) = c.sides else {
let Some(sides) = equality_sides(c.g, ctx) else {
continue;
};
push_equality_gate(&mut gates, &mut reach, c.g, c.clause_ix, c.key, sides);
continue;
};
push_equality_gate(
&mut gates,
&mut reach,
c.g,
c.clause_ix,
c.key,
(pos_l, keys_l, pos_r, keys_r),
);
continue;
}
};
narrow_reach(&mut reach, &c.g.sym_l, index.len());
narrow_reach(&mut reach, &c.g.sym_r, index.len());
gates.push(JoinGate {
sym_src: c.g.sym_l.clone(),
sym_tgt: c.g.sym_r.clone(),
index,
n_src,
n_tgt,
clause_ix: c.clause_ix,
});
}
gates.sort_by(JoinGate::selectivity_cmp);
gates
}
pub(super) type EqSides = (
Vec<i64>,
Vec<crate::relational::Key>,
Vec<i64>,
Vec<crate::relational::Key>,
);
struct EqCandidate<'a> {
g: &'a crate::join::OnGate,
clause_ix: usize,
matches: usize,
n_l: usize,
n_r: usize,
key: EqGateKey,
ready: Option<EqGateEntry>,
sides: Option<EqSides>,
}
fn push_equality_gate(
gates: &mut Vec<JoinGate>,
reach: &mut HashMap<String, usize>,
g: &crate::join::OnGate,
clause_ix: usize,
key: EqGateKey,
sides: EqSides,
) {
EQ_GATE_CACHE.with(|c| {
c.borrow_mut()
.evict_to_budget(crate::broad_phase::gate_cache_pair_budget())
});
crate::broad_phase::bump_gate_index_builds();
let (ix, n_src, n_tgt) = index_from_sides(sides);
let entry: EqGateEntry = (Rc::new(ix), n_src, n_tgt);
EQ_GATE_CACHE.with(|c| c.borrow_mut().insert(key, Some(entry.clone())));
narrow_reach(reach, &g.sym_l, entry.0.len());
narrow_reach(reach, &g.sym_r, entry.0.len());
gates.push(JoinGate {
sym_src: g.sym_l.clone(),
sym_tgt: g.sym_r.clone(),
index: entry.0,
n_src,
n_tgt,
clause_ix,
});
}
pub(super) fn reach_of(reach: &HashMap<String, usize>, sym: &str, full: usize) -> usize {
reach.get(sym).map_or(full, |r| (*r).min(full))
}
pub(super) fn narrow_reach(reach: &mut HashMap<String, usize>, sym: &str, pairs: usize) {
let slot = reach.entry(sym.to_string()).or_insert(usize::MAX);
*slot = (*slot).min(pairs);
}
fn build_overlap_index(
src_env: &[String],
tgt_env: &[String],
eps: f64,
ctx: &EvalCtx,
) -> Option<crate::broad_phase::OverlapIndex> {
let mut arrays: HashMap<String, ArrayD<f64>> = HashMap::new();
for name in src_env.iter().chain(tgt_env.iter()) {
if arrays.contains_key(name) {
continue;
}
match lookup_variable(name, ctx) {
Value::Array(a) => {
arrays.insert(name.clone(), *a);
}
Value::Scalar(_) => return None,
}
}
let src = crate::broad_phase::envelope_vectors(src_env, &arrays).ok()?;
let tgt = crate::broad_phase::envelope_vectors(tgt_env, &arrays).ok()?;
let pairs = crate::broad_phase::broad_phase_candidates(&src, &tgt, eps);
Some(crate::broad_phase::OverlapIndex::from_zero_based(&pairs))
}
type EqGateKey = (u64, SmallVec<[usize; 2]>, u64);
type EqGateEntry = (Rc<crate::broad_phase::OverlapIndex>, usize, usize);
thread_local! {
static EQ_GATE_CACHE: RefCell<GateCache<EqGateKey, Option<EqGateEntry>>> =
RefCell::new(GateCache::new());
}
thread_local! {
static EQ_PRICE_CACHE: RefCell<HashMap<EqGateKey, (usize, usize, usize)>> =
RefCell::new(HashMap::new());
}
fn remember_price(key: EqGateKey, price: (usize, usize, usize)) {
EQ_PRICE_CACHE.with(|c| {
let mut m = c.borrow_mut();
if m.len() >= GATE_CACHE_MAX_ENTRIES {
m.clear();
}
m.insert(key, price);
});
}
fn with_named_array<R>(name: &str, ctx: &EvalCtx, f: impl FnOnce(&ArrayD<f64>) -> R) -> Option<R> {
if let Some(a) = ctx.state_arrays.get(name) {
return Some(f(a));
}
if let Some(a) = ctx.observed_arrays.get(name) {
return Some(f(a));
}
let forcing = ctx.forcing.borrow();
forcing.get(name).map(f)
}
fn column_fingerprint(a: &ArrayD<f64>, acc: &mut u64) {
for v in a.iter() {
*acc ^= v.to_bits();
*acc = acc.wrapping_mul(0x0000_0100_0000_01B3);
}
}
fn key_column_values(
col: &crate::join::KeyColumn,
ctx: &EvalCtx,
) -> Option<(Vec<i64>, Vec<crate::relational::Key>)> {
use crate::join::KeyColumn;
match col {
KeyColumn::Const { positions, values } => Some(const_key_column(positions, values)),
KeyColumn::Column(name) => with_named_array(name, ctx, data_key_column)?,
}
}
pub(super) fn const_key_column(
positions: &[i64],
values: &[crate::join::JoinKey],
) -> (Vec<i64>, Vec<crate::relational::Key>) {
use crate::join::JoinKey;
let keys = values
.iter()
.map(|v| match v {
JoinKey::Int(i) => crate::relational::Key::Int(*i),
JoinKey::Cat(c) => crate::relational::Key::Str(c.clone()),
})
.collect();
(positions.to_vec(), keys)
}
pub(super) fn data_key_column(a: &ArrayD<f64>) -> Option<(Vec<i64>, Vec<crate::relational::Key>)> {
if a.ndim() != 1 {
return None;
}
let mut keys = Vec::with_capacity(a.len());
for &v in a.iter() {
if !v.is_finite() || v.fract() != 0.0 {
return None;
}
keys.push(crate::relational::Key::Int(v as i64));
}
Some(((1..=a.len() as i64).collect(), keys))
}
fn side_keys(
cols: &[crate::join::KeyColumn],
ctx: &EvalCtx,
) -> Option<(Vec<i64>, Vec<crate::relational::Key>)> {
let parts = cols
.iter()
.map(|c| key_column_values(c, ctx))
.collect::<Option<Vec<_>>>()?;
composite_side_keys(parts)
}
pub(super) fn composite_side_keys(
mut parts: Vec<(Vec<i64>, Vec<crate::relational::Key>)>,
) -> Option<(Vec<i64>, Vec<crate::relational::Key>)> {
if parts.len() == 1 {
return parts.pop();
}
let (positions, _) = parts.first()?;
let positions = positions.clone();
if parts.iter().any(|(p, _)| *p != positions) {
return None;
}
let keys = (0..positions.len())
.map(|t| crate::relational::skolem(parts.iter().map(|p| p.1[t].clone()).collect(), false))
.collect();
Some((positions, keys))
}
fn equality_cache_key(g: &crate::join::OnGate, ctx: &EvalCtx) -> Option<EqGateKey> {
use crate::join::KeyColumn;
let mut lens: SmallVec<[usize; 2]> = SmallVec::new();
let mut fp: u64 = 0xcbf2_9ce4_8422_2325;
for c in g.cols_l.iter().chain(g.cols_r.iter()) {
if let KeyColumn::Column(name) = c {
let n = with_named_array(name, ctx, |a| {
column_fingerprint(a, &mut fp);
a.len()
})?;
lens.push(n);
}
}
Some((g.id, lens, fp))
}
fn equality_sides(g: &crate::join::OnGate, ctx: &EvalCtx) -> Option<EqSides> {
let (pos_l, keys_l) = side_keys(&g.cols_l, ctx)?;
let (pos_r, keys_r) = side_keys(&g.cols_r, ctx)?;
Some((pos_l, keys_l, pos_r, keys_r))
}
pub(super) fn index_from_sides(sides: EqSides) -> (crate::broad_phase::OverlapIndex, usize, usize) {
let (pos_l, keys_l, pos_r, keys_r) = sides;
let (n_l, n_r) = (pos_l.len(), pos_r.len());
let pairs: Vec<(i64, i64)> = crate::relational::equijoin(&keys_l, &keys_r)
.into_iter()
.map(|(i, j)| (pos_l[i], pos_r[j]))
.collect();
(
crate::broad_phase::OverlapIndex::from_owned_pairs(pairs),
n_l,
n_r,
)
}
enum DimSrc<'a> {
Range(i64, i64),
List(&'a [i64]),
}
impl DimSrc<'_> {
#[inline]
fn len(&self) -> usize {
match self {
DimSrc::Range(lo, hi) => (hi - lo + 1).max(0) as usize,
DimSrc::List(v) => v.len(),
}
}
#[inline]
fn at(&self, i: usize) -> i64 {
match self {
DimSrc::Range(lo, _) => lo + i as i64,
DimSrc::List(v) => v[i],
}
}
}
enum Restriction<'a> {
Slice(&'a [i64]),
Owned(Vec<i64>),
}
impl Restriction<'_> {
#[inline]
fn as_slice(&self) -> &[i64] {
match self {
Restriction::Slice(s) => s,
Restriction::Owned(v) => v,
}
}
}
fn intersect_restriction<'a>(slot: &mut Option<Restriction<'a>>, parts: &'a [i64]) -> bool {
match slot.take() {
None => {
let ok = !parts.is_empty();
*slot = Some(Restriction::Slice(parts));
ok
}
Some(cur) => {
let a = cur.as_slice();
let mut out: Vec<i64> = Vec::with_capacity(a.len().min(parts.len()));
let (mut i, mut j) = (0usize, 0usize);
while i < a.len() && j < parts.len() {
match a[i].cmp(&parts[j]) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
out.push(a[i]);
i += 1;
j += 1;
}
}
}
let ok = !out.is_empty();
*slot = Some(Restriction::Owned(out));
ok
}
}
}
pub(super) fn reduce_contraction_gated(
spec: &ReduceSpec,
ranges: &[(i64, i64)],
gates: &[(JoinGate, GatePlacement)],
ctx: &mut EvalCtx,
) -> f64 {
use crate::broad_phase::Side;
let &ReduceSpec {
contract_names,
reduce,
..
} = spec;
let mut restrict: SmallVec<[Option<Restriction<'_>>; 4]> =
(0..ranges.len()).map(|_| None).collect();
let mut pair_gate: Option<(&JoinGate, usize, usize, bool)> = None;
let mut placed = false;
for (g, place) in gates {
match (place.src, place.tgt) {
(GateAxis::Output, GateAxis::Output) => {
let (Some(&l), Some(&r)) = (
ctx.loop_binds.get(&g.sym_src),
ctx.loop_binds.get(&g.sym_tgt),
) else {
continue;
};
placed = true;
if !g.index.contains(l, r) {
return reduce.identity();
}
}
(GateAxis::Contracted(d), GateAxis::Output) => {
let (Some(&r), Some(&(lo, hi))) = (ctx.loop_binds.get(&g.sym_tgt), ranges.get(d))
else {
continue;
};
placed = true;
let parts = g.index.partners_in(Side::Tgt, r, lo, hi);
if !intersect_restriction(&mut restrict[d], parts) {
return reduce.identity();
}
}
(GateAxis::Output, GateAxis::Contracted(d)) => {
let (Some(&l), Some(&(lo, hi))) = (ctx.loop_binds.get(&g.sym_src), ranges.get(d))
else {
continue;
};
placed = true;
let parts = g.index.partners_in(Side::Src, l, lo, hi);
if !intersect_restriction(&mut restrict[d], parts) {
return reduce.identity();
}
}
(GateAxis::Contracted(a), GateAxis::Contracted(b)) if a != b => {
placed = true;
if pair_gate.is_none() {
let (p, q, bound_is_src) = if a < b { (a, b, true) } else { (b, a, false) };
pair_gate = Some((g, p, q, bound_is_src));
}
}
_ => {}
}
}
let mut srcs = full_sources(ranges);
if placed {
for (d, r) in restrict.iter().enumerate() {
if let Some(r) = r {
srcs[d] = DimSrc::List(r.as_slice());
}
}
}
let Some((gate, p, q, bound_is_src)) = pair_gate else {
return reduce_over_sources(spec, &srcs, ctx);
};
if contract_names.len() == 2 && restrict[0].is_none() && restrict[1].is_none() {
let src_is_slow = bound_is_src;
let (lo0, hi0) = ranges[0];
let (lo1, hi1) = ranges[1];
let mut tuples: Vec<(i64, i64)> = gate
.index
.pairs()
.map(|(l, r)| if src_is_slow { (l, r) } else { (r, l) })
.filter(|&(a, b)| a >= lo0 && a <= hi0 && b >= lo1 && b <= hi1)
.collect();
if !src_is_slow {
tuples.sort_unstable();
}
return reduce_over_pairs(spec, &tuples, ctx);
}
let mut acc = reduce.identity();
drive_partner_restricted(spec, &srcs, gate, p, q, bound_is_src, 0, &mut acc, ctx);
acc
}
#[allow(clippy::too_many_arguments)]
fn drive_partner_restricted(
spec: &ReduceSpec,
srcs: &[DimSrc],
gate: &JoinGate,
p: usize,
q: usize,
bound_is_src: bool,
d: usize,
acc: &mut f64,
ctx: &mut EvalCtx,
) {
let &ReduceSpec {
contract_names,
body,
reduce,
filter,
cell,
} = spec;
if d == srcs.len() {
crate::broad_phase::bump_overlap_enum_visits(1);
if !filter_excludes(filter, cell, ctx) {
let term = eval(body, ctx).as_scalar().unwrap_or(f64::NAN);
*acc = reduce.combine(*acc, term);
}
return;
}
if d == q {
let Some(&bound) = ctx.loop_binds.get(&contract_names[p]) else {
return;
};
let side = if bound_is_src {
crate::broad_phase::Side::Src
} else {
crate::broad_phase::Side::Tgt
};
match &srcs[q] {
DimSrc::Range(lo, hi) => {
let parts = gate.index.partners_in(side, bound, *lo, *hi);
for i in 0..parts.len() {
let v = parts[i];
set_bind(&mut ctx.loop_binds, &contract_names[d], v);
drive_partner_restricted(spec, srcs, gate, p, q, bound_is_src, d + 1, acc, ctx);
}
}
DimSrc::List(vals) => {
let parts = gate.index.partners(side, bound);
let (mut i, mut j) = (0usize, 0usize);
while i < vals.len() && j < parts.len() {
match vals[i].cmp(&parts[j]) {
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
std::cmp::Ordering::Equal => {
set_bind(&mut ctx.loop_binds, &contract_names[d], vals[i]);
drive_partner_restricted(
spec,
srcs,
gate,
p,
q,
bound_is_src,
d + 1,
acc,
ctx,
);
i += 1;
j += 1;
}
}
}
}
}
return;
}
let s = &srcs[d];
for i in 0..s.len() {
set_bind(&mut ctx.loop_binds, &contract_names[d], s.at(i));
drive_partner_restricted(spec, srcs, gate, p, q, bound_is_src, d + 1, acc, ctx);
}
}
fn full_sources(ranges: &[(i64, i64)]) -> SmallVec<[DimSrc<'_>; 4]> {
ranges
.iter()
.map(|&(lo, hi)| DimSrc::Range(lo, hi))
.collect()
}
fn reduce_over_sources(spec: &ReduceSpec, srcs: &[DimSrc], ctx: &mut EvalCtx) -> f64 {
let &ReduceSpec {
contract_names,
body,
reduce,
filter,
cell,
} = spec;
let mut acc = reduce.identity();
let n = srcs.len();
if n == 0 {
crate::broad_phase::bump_overlap_enum_visits(1);
return if filter_excludes(filter, cell, ctx) {
acc
} else {
eval(body, ctx).as_scalar().unwrap_or(f64::NAN)
};
}
if srcs.iter().any(|s| s.len() == 0) {
return acc;
}
let mut odom: SmallVec<[usize; 4]> = SmallVec::from_elem(0usize, n);
loop {
crate::broad_phase::bump_overlap_enum_visits(1);
for (d, name) in contract_names.iter().enumerate() {
set_bind(&mut ctx.loop_binds, name, srcs[d].at(odom[d]));
}
if !filter_excludes(filter, cell, ctx) {
let term = eval(body, ctx).as_scalar().unwrap_or(f64::NAN);
acc = reduce.combine(acc, term);
}
let mut d = n;
loop {
if d == 0 {
return acc;
}
d -= 1;
odom[d] += 1;
if odom[d] < srcs[d].len() {
break;
}
odom[d] = 0;
}
}
}
fn reduce_over_pairs(spec: &ReduceSpec, tuples: &[(i64, i64)], ctx: &mut EvalCtx) -> f64 {
let &ReduceSpec {
contract_names,
body,
reduce,
filter,
cell,
} = spec;
let mut acc = reduce.identity();
for &(a, b) in tuples {
crate::broad_phase::bump_overlap_enum_visits(1);
set_bind(&mut ctx.loop_binds, &contract_names[0], a);
set_bind(&mut ctx.loop_binds, &contract_names[1], b);
if filter_excludes(filter, cell, ctx) {
continue;
}
let term = eval(body, ctx).as_scalar().unwrap_or(f64::NAN);
acc = reduce.combine(acc, term);
}
acc
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct PrefixScan {
pub axis: usize,
pub inclusive: bool,
pub reverse: bool,
}
pub(super) fn detect_prefix_scan(
output_idx_names: &[String],
output_ranges: &[(i64, i64)],
contract_names: &[String],
static_ranges: Option<&[(i64, i64)]>,
body: &Expr,
filter: Option<&Expr>,
) -> Option<PrefixScan> {
let filter = filter?;
let [j_name] = contract_names else {
return None;
};
let [(c_lo, c_hi)] = *static_ranges? else {
return None;
};
let ScanWindow {
axis,
inclusive,
reverse,
} = scan_window(output_idx_names, j_name, filter)?;
let i_name = &output_idx_names[axis];
if output_ranges.get(axis)? != &(c_lo, c_hi) {
return None;
}
if expr_references(body, i_name) {
return None;
}
Some(PrefixScan {
axis,
inclusive,
reverse,
})
}
pub(super) struct ScanWindow {
pub axis: usize,
pub inclusive: bool,
pub reverse: bool,
}
pub(super) fn scan_window(
output_idx_names: &[String],
j_name: &str,
filter: &Expr,
) -> Option<ScanWindow> {
let Expr::Operator(node) = filter else {
return None;
};
if node.args.len() != 2 {
return None;
}
let (lhs, rhs) = (var_name(&node.args[0])?, var_name(&node.args[1])?);
let (low, high, inclusive) = match node.op.as_str() {
"<=" | "<" => (lhs, rhs, node.op == "<="),
">=" | ">" => (rhs, lhs, node.op == ">="),
_ => return None,
};
let (i_name, reverse) = if low == j_name {
(high, false)
} else if high == j_name {
(low, true)
} else {
return None;
};
let axis = output_idx_names.iter().position(|s| s == i_name)?;
Some(ScanWindow {
axis,
inclusive,
reverse,
})
}
pub(super) fn folds_from_the_top(
output_idx_names: &[String],
contract_names: &[String],
filter: Option<&Expr>,
) -> bool {
let (Some(filter), [j_name]) = (filter, contract_names) else {
return false;
};
scan_window(output_idx_names, j_name, filter).is_some_and(|w| w.reverse)
}
fn var_name(e: &Expr) -> Option<&str> {
match e {
Expr::Variable(name) => Some(name.as_str()),
_ => None,
}
}
fn expr_references(e: &Expr, name: &str) -> bool {
match e {
Expr::Variable(v) => v == name,
Expr::Integer(_) | Expr::Number(_) => false,
Expr::Operator(node) => {
node.args.iter().any(|a| expr_references(a, name))
|| [
node.expr.as_deref(),
node.filter.as_deref(),
node.key.as_deref(),
node.lower.as_deref(),
node.upper.as_deref(),
]
.into_iter()
.flatten()
.any(|c| expr_references(c, name))
|| node
.values
.as_ref()
.is_some_and(|vs| vs.iter().any(|v| expr_references(v, name)))
}
}
}
pub(super) struct ScanSweep<'a> {
pub(super) scan: PrefixScan,
pub(super) i_name: &'a str,
pub(super) j_name: &'a str,
pub(super) bounds: (i64, i64),
pub(super) body: &'a Expr,
pub(super) reduce: ReduceKind,
}
pub(super) fn run_prefix_scan(
sweep: &ScanSweep,
ctx: &mut EvalCtx,
mut emit: impl FnMut(i64, f64, &EvalCtx),
) {
let &ScanSweep {
scan,
i_name,
j_name,
bounds: (lo, hi),
body,
reduce,
} = sweep;
let term_at = |i: i64, ctx: &mut EvalCtx| {
set_bind(&mut ctx.loop_binds, i_name, i);
set_bind(&mut ctx.loop_binds, j_name, i);
eval(body, ctx).as_scalar().unwrap_or(f64::NAN)
};
let mut acc = reduce.identity();
let steps: &mut dyn Iterator<Item = i64> = if scan.reverse {
&mut (lo..=hi).rev()
} else {
&mut (lo..=hi)
};
for i in steps {
if scan.inclusive {
let term = term_at(i, ctx);
acc = reduce.combine(acc, term);
set_bind(&mut ctx.loop_binds, i_name, i);
emit(i, acc, ctx);
} else {
set_bind(&mut ctx.loop_binds, i_name, i);
emit(i, acc, ctx);
let term = term_at(i, ctx);
acc = reduce.combine(acc, term);
}
}
}
pub(super) fn static_contract_ranges(
contract_dims: &[ContractDim],
) -> Option<SmallVec<[(i64, i64); 4]>> {
contract_dims
.iter()
.map(|d| d.static_bound())
.collect::<Option<SmallVec<[(i64, i64); 4]>>>()
}
pub(super) fn ragged_upper_bound(offsets: &str, of: &[String], ctx: &EvalCtx) -> i64 {
let arr = match lookup_variable(offsets, ctx) {
Value::Scalar(s) => return s.round() as i64,
Value::Array(a) => a,
};
if of.len() != arr.ndim() {
return 0;
}
let mut idx = Vec::with_capacity(of.len());
for p in of {
match ctx.loop_binds.get(p) {
Some(pv) if *pv >= 1 => idx.push((*pv - 1) as usize),
_ => return 0,
}
}
arr.get(IxDyn(&idx)).map(|v| v.round() as i64).unwrap_or(0)
}
thread_local! {
static ARRAYOP_POOL: RefCell<Pool> = RefCell::new(Pool::default());
}
pub(super) fn with_faq_pool<R>(f: impl FnOnce(&mut Pool) -> R) -> R {
ARRAYOP_POOL.with(|p| match p.try_borrow_mut() {
Ok(mut pool) => f(&mut pool),
Err(_) => f(&mut Pool::default()),
})
}
pub(super) struct ArrayOpSpec<'n> {
pub(super) idx_names: &'n [String],
pub(super) ranges: RangeVec,
pub(super) body: &'n Expr,
pub(super) contract_names: Vec<String>,
pub(super) contract_dims: Vec<ContractDim>,
pub(super) reduce: ReduceKind,
pub(super) filter: Option<&'n Expr>,
pub(super) join: Option<&'n [JoinClause]>,
}
impl ArrayOpSpec<'_> {
pub(super) fn has_drivable_overlap(&self) -> bool {
self.join.is_some_and(|j| {
j.iter().any(|c| {
c.on_gate.is_some()
|| c.overlap
.as_ref()
.is_some_and(|o| o.sym_src.is_some() && o.sym_tgt.is_some())
})
})
}
}
pub(super) fn faq_spec(node: &ExpressionNode) -> Option<ArrayOpSpec<'_>> {
let idx_names: &[String] = node.output_idx.as_deref().unwrap_or(&[]);
static EMPTY_RANGES: std::sync::OnceLock<HashMap<String, crate::types::RangeSpec>> =
std::sync::OnceLock::new();
let ranges_map = node
.ranges
.as_ref()
.unwrap_or_else(|| EMPTY_RANGES.get_or_init(HashMap::new));
let body: &Expr = node.expr.as_deref()?;
let ranges: RangeVec = idx_names
.iter()
.map(|n| {
let r = ranges_map.get(n).and_then(|s| s.bounds()).unwrap_or([0, 0]);
(r[0], r[1])
})
.collect();
let mut sorted_contract_keys: Vec<&String> = ranges_map
.keys()
.filter(|k| !idx_names.iter().any(|n| n == *k))
.collect();
sorted_contract_keys.sort();
let contract_names: Vec<String> = sorted_contract_keys.iter().map(|k| (*k).clone()).collect();
let contract_dims: Vec<ContractDim> = sorted_contract_keys
.iter()
.map(|k| ContractDim::from_range(&ranges_map[*k]))
.collect();
let reduce = effective_reduce_kind(node.semiring.as_deref(), node.reduce.as_deref()).ok()?;
let filter = node.filter.as_deref();
Some(ArrayOpSpec {
idx_names,
ranges,
body,
contract_names,
contract_dims,
reduce,
filter,
join: node.join.as_deref(),
})
}
pub(super) fn eval_faq(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
let spec = match faq_spec(node) {
Some(s) => s,
None => return Value::Scalar(f64::NAN),
};
let gates: Vec<(JoinGate, GatePlacement)> = spec
.join
.map(|j| resolve_join_gates(j, &*ctx))
.unwrap_or_default()
.into_iter()
.map(|g| {
let place = gate_placement(&g, spec.idx_names, &spec.contract_names);
(g, place)
})
.collect();
let ArrayOpSpec {
idx_names,
ranges,
body,
contract_names,
contract_dims,
reduce,
filter,
join: _,
} = spec;
let shape: DimU = ranges
.iter()
.map(|(lo, hi)| (hi - lo + 1) as usize)
.collect();
let origin: DimI = ranges.iter().map(|(lo, _)| *lo).collect();
let total = shape.iter().copied().product::<usize>().max(1);
let static_ranges = static_contract_ranges(&contract_dims);
let scan = detect_prefix_scan(
idx_names,
&ranges,
&contract_names,
static_ranges.as_deref(),
body,
filter,
);
if !shape.is_empty() && scan.is_none() && gates.is_empty() && ctx.recur.is_none() {
let materialized = with_faq_pool(|pool| {
try_eval_faq_vectorized(
idx_names,
&ranges,
body,
&contract_names,
&contract_dims,
reduce,
filter,
&*ctx,
pool,
)
.map(|(vv, _ops)| {
let out = vv.view().expect("vectorized faq has a view").to_owned();
vv.release(pool);
out
})
});
if let Some(out) = materialized {
return Value::Array(Box::new(out));
}
}
if shape.is_empty()
&& !contract_names.is_empty()
&& filter.is_none()
&& scan.is_none()
&& gates.is_empty()
&& ctx.recur.is_none()
&& let Some(box_ranges) = static_ranges.as_deref()
{
let terms = with_faq_pool(|pool| {
try_eval_faq_vectorized(
&contract_names,
box_ranges,
body,
&[],
&[],
reduce,
None,
&*ctx,
pool,
)
.map(|(vv, _ops)| {
let out = vv.view().expect("vectorized faq has a view").to_owned();
vv.release(pool);
out
})
});
if let Some(terms) = terms {
let acc = terms
.iter()
.fold(reduce.identity(), |acc, &t| reduce.combine(acc, t));
return Value::Scalar(acc);
}
}
if shape.contains(&0) {
return Value::Array(Box::new(ArrayD::zeros(IxDyn(&shape))));
}
let is_walk = !shape.is_empty() || !contract_names.is_empty();
if is_walk {
if !gates.is_empty() {
super::vectorized::note_overlay_decline(
"faq: a join gate (the overlay takes no gated `faq`; the gate drives a \
per-cell enumeration)",
);
} else if ctx.recur.is_some() {
super::vectorized::note_overlay_decline(
"faq: inside a recurrence sweep (CONFORMANCE_SPEC §5.19.2)",
);
}
note_per_cell_walk();
if per_cell_walk_stopped() {
return if shape.is_empty() {
Value::Scalar(0.0)
} else {
Value::Array(Box::new(ArrayD::zeros(IxDyn(&shape))))
};
}
}
let mut buf = vec![0.0f64; total];
let saved_binds: Vec<(String, Option<i64>)> = idx_names
.iter()
.chain(contract_names.iter())
.map(|n| (n.clone(), ctx.loop_binds.get(n).copied()))
.collect();
if let Some(scan) = scan {
let (scan_lo, scan_hi) = ranges[scan.axis];
let outer_ranges: Vec<(i64, i64)> = ranges
.iter()
.enumerate()
.filter(|(d, _)| *d != scan.axis)
.map(|(_, r)| *r)
.collect();
let outer_axes: Vec<(usize, &String)> = idx_names
.iter()
.enumerate()
.filter(|(d, _)| *d != scan.axis)
.collect();
let sweep = ScanSweep {
scan,
i_name: &idx_names[scan.axis],
j_name: &contract_names[0],
bounds: if !stop_at_first_cell() {
(scan_lo, scan_hi)
} else if scan.reverse {
(scan_lo.max(scan_hi), scan_hi)
} else {
(scan_lo, scan_hi.min(scan_lo))
},
body,
reduce,
};
let mut full = vec![0i64; ranges.len()];
let mut outer = CartesianTuples::new(&outer_ranges);
while let Some(otuple) = outer.next() {
for ((d, name), val) in outer_axes.iter().zip(otuple.iter()) {
set_bind(&mut ctx.loop_binds, name, *val);
full[*d] = *val;
}
run_prefix_scan(&sweep, ctx, |i, acc, _| {
note_per_cell_cell();
full[scan.axis] = i;
buf[multi_to_flat_col_major(&full, &shape, &origin)] = acc;
});
if stop_after_first_cell() {
break;
}
}
} else {
let cellbox = CellBox {
names: idx_names,
origin: &origin,
};
let spec = ReduceSpec {
contract_names: &contract_names,
body,
reduce,
filter,
cell: Some(&cellbox),
};
let stats_from = (!gates.is_empty()).then(crate::broad_phase::overlap_enum_visits);
let mut tuples = CartesianTuples::new(&ranges);
while let Some(tuple) = tuples.next() {
for (name, val) in idx_names.iter().zip(tuple.iter()) {
set_bind(&mut ctx.loop_binds, name, *val);
}
let v = if gates.is_empty() {
reduce_contraction(&spec, &contract_dims, static_ranges.as_deref(), ctx)
} else {
let derived: SmallVec<[(i64, i64); 4]>;
let cell_ranges: &[(i64, i64)] = match static_ranges.as_deref() {
Some(r) => r,
None => {
derived = contract_dims.iter().map(|d| d.concrete(ctx)).collect();
&derived
}
};
reduce_contraction_gated(&spec, cell_ranges, &gates, ctx)
};
let flat = multi_to_flat_col_major(tuple, &shape, &origin);
buf[flat] = v;
if is_walk {
note_per_cell_cell();
if stop_after_first_cell() {
break;
}
}
}
if let Some(before) = stats_from {
let desc = gates
.iter()
.map(|(g, _)| format!("{}~{}:{}", g.sym_src, g.sym_tgt, g.index.len()))
.collect::<Vec<_>>()
.join(",");
eprintln!(
"[join-gate] gates={desc} cells={total} leaves={}",
crate::broad_phase::overlap_enum_visits().wrapping_sub(before),
);
}
}
for (name, saved) in saved_binds {
match saved {
Some(v) => {
ctx.loop_binds.insert(name, v);
}
None => {
ctx.loop_binds.remove(&name);
}
}
}
if shape.is_empty() {
Value::Scalar(buf[0])
} else {
Value::Array(Box::new(col_major_to_arrayd(&buf, &shape)))
}
}
pub(super) fn region_value_is_prefix_scan(value: &Expr) -> bool {
let Expr::Operator(n) = value else {
return false;
};
if n.filter.is_none() {
return false;
}
let Some(spec) = faq_spec(n) else {
return false;
};
let static_ranges = static_contract_ranges(&spec.contract_dims);
detect_prefix_scan(
spec.idx_names,
&spec.ranges,
&spec.contract_names,
static_ranges.as_deref(),
spec.body,
spec.filter,
)
.is_some()
}
pub(super) fn eval_makearray(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
let regions: &[Vec<[crate::types::RegionBound; 2]>] = node.regions.as_deref().unwrap_or(&[]);
let values: &[Expr] = node.values.as_deref().unwrap_or(&[]);
if regions.is_empty() || values.len() != regions.len() {
return Value::Scalar(f64::NAN);
}
if crate::op_registry::check_makearray_regions(node).is_err() {
return Value::Scalar(f64::NAN);
}
let ndim = regions[0].len();
let mut lo = vec![i64::MAX; ndim];
let mut hi = vec![i64::MIN; ndim];
for region in regions {
for (d, r) in region.iter().enumerate() {
let Some([r_lo, r_hi]) = crate::types::region_bounds(r) else {
return Value::Scalar(f64::NAN);
};
lo[d] = lo[d].min(r_lo);
hi[d] = hi[d].max(r_hi);
}
}
let shape: Vec<usize> = (0..ndim)
.map(|d| (hi[d] - lo[d] + 1).max(0) as usize)
.collect();
let origin = lo.clone();
if !overlay_off()
&& ctx.loop_binds.is_empty()
&& !shape.contains(&0)
&& !values.iter().any(region_value_is_prefix_scan)
&& ctx.recur.is_none()
{
let bx = VecBox {
syms: &[],
lo: &lo,
shape: &shape,
cnames: &[],
cvals: &[],
};
let materialized = with_faq_pool(|pool| {
let mut ops = 0usize;
eval_vec_makearray(node, &bx, &*ctx, pool, &mut ops).map(|vv| {
let out = vv
.view()
.expect("vectorized makearray has a view")
.to_owned();
vv.release(pool);
out
})
});
if let Some(out) = materialized {
return Value::Array(Box::new(out));
}
}
let mut arr = ArrayD::<f64>::zeros(IxDyn(&shape));
for (region, value_expr) in regions.iter().zip(values.iter()) {
let v = eval(value_expr, ctx);
let mut ranges: Vec<(i64, i64)> = Vec::with_capacity(region.len());
for r in region {
let Some([r_lo, r_hi]) = crate::types::region_bounds(r) else {
return Value::Scalar(f64::NAN);
};
ranges.push((r_lo, r_hi));
}
if let Value::Array(a) = &v
&& a.ndim() > 0
{
let region_shape: Vec<usize> = ranges
.iter()
.map(|(lo, hi)| (hi - lo + 1).max(0) as usize)
.collect();
let Some(covered) = region_value_axes(a.shape(), ®ion_shape) else {
return Value::Scalar(f64::NAN);
};
if region_shape.contains(&0) {
continue;
}
arr.slice_each_axis_mut(|ax| {
let d = ax.axis.index();
let s0 = (ranges[d].0 - origin[d]) as usize;
ndarray::Slice::from(s0..s0 + region_shape[d])
})
.assign(®ion_value_view(a.view(), &covered));
continue;
}
let scalar = match &v {
Value::Scalar(s) => *s,
Value::Array(a) if a.ndim() == 0 => a[IxDyn(&[])],
_ => continue,
};
let region_shape: Vec<usize> = ranges
.iter()
.map(|(lo, hi)| (hi - lo + 1).max(0) as usize)
.collect();
if region_shape.contains(&0) {
continue;
}
arr.slice_each_axis_mut(|ax| {
let d = ax.axis.index();
let s0 = (ranges[d].0 - origin[d]) as usize;
ndarray::Slice::from(s0..s0 + region_shape[d])
})
.fill(scalar);
}
Value::Array(Box::new(arr))
}
pub(super) fn eval_reshape(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
let Some(arg0) = node.args.first() else {
return Value::Scalar(f64::NAN);
};
let v = eval(arg0, ctx);
let arr = match v {
Value::Array(a) => *a,
Value::Scalar(s) => ArrayD::from_elem(IxDyn(&[]), s),
};
let target: Vec<usize> = node
.shape
.clone()
.unwrap_or_default()
.iter()
.map(|&d| d as usize)
.collect();
let flat = arrayd_to_col_major(&arr);
if target.iter().product::<usize>() != flat.len() {
return Value::Scalar(f64::NAN);
}
Value::Array(Box::new(col_major_to_arrayd(&flat, &target)))
}
fn is_valid_permutation(perm: &[usize], ndim: usize) -> bool {
if perm.len() != ndim {
return false;
}
let mut seen = vec![false; ndim];
for &ax in perm {
if ax >= ndim || seen[ax] {
return false;
}
seen[ax] = true;
}
true
}
pub(super) fn eval_transpose(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
let Some(arg0) = node.args.first() else {
return Value::Scalar(f64::NAN);
};
let v = eval(arg0, ctx);
let arr = match v {
Value::Array(a) => a,
Value::Scalar(s) => return Value::Scalar(s),
};
let perm: Vec<usize> = if let Some(p) = &node.perm {
p.iter().map(|&x| x as usize).collect()
} else {
(0..arr.ndim()).rev().collect()
};
if !is_valid_permutation(&perm, arr.ndim()) {
return Value::Scalar(f64::NAN);
}
Value::Array(Box::new(
arr.permuted_axes(perm).as_standard_layout().into_owned(),
))
}
pub(super) fn eval_concat(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
let axis = node.axis.unwrap_or(0) as usize;
let parts: Vec<ArrayD<f64>> = node
.args
.iter()
.map(|a| match eval(a, ctx) {
Value::Array(arr) => *arr,
Value::Scalar(s) => ArrayD::from_elem(IxDyn(&[1]), s),
})
.collect();
let views: Vec<_> = parts.iter().map(|a| a.view()).collect();
match ndarray::concatenate(ndarray::Axis(axis), &views) {
Ok(joined) => Value::Array(Box::new(joined)),
Err(_) => Value::Scalar(f64::NAN),
}
}
pub(super) fn eval_broadcast(node: &ExpressionNode, ctx: &mut EvalCtx) -> Value {
let Some(fn_name) = node.broadcast_fn.as_deref() else {
unreachable!(
"`broadcast` reached the evaluator with no `fn`; every entry point must gate \
with op_registry::check_broadcast_fn() first"
)
};
assert!(
crate::op_registry::is_scalar_operator(fn_name),
"`broadcast` fn '{fn_name}' is not a scalar operator and reached the evaluator; \
every entry point must gate with op_registry::check_broadcast_fn() first"
);
eval_op_named(fn_name, node, ctx)
}
pub(super) fn eval_simple_index<S: std::hash::BuildHasher>(
expr: &Expr,
binds: &HashMap<String, i64, S>,
) -> i64 {
match expr {
Expr::Integer(n) => *n,
Expr::Number(n) => *n as i64,
Expr::Variable(name) => binds.get(name).copied().unwrap_or(0),
Expr::Operator(node) if (node.op == "+" || node.op == "-") && node.args.len() == 2 => {
let a = eval_simple_index(&node.args[0], binds);
let b = eval_simple_index(&node.args[1], binds);
if node.op == "+" { a + b } else { a - b }
}
_ => 0,
}
}
pub(super) fn evaluate_index_range(
expr: &Expr,
loop_ranges: &HashMap<String, (i64, i64)>,
) -> (i64, i64) {
match expr {
Expr::Integer(n) => (*n, *n),
Expr::Number(n) => {
let v = *n as i64;
(v, v)
}
Expr::Variable(name) => {
if let Some((lo, hi)) = loop_ranges.get(name) {
(*lo, *hi)
} else {
(0, 0)
}
}
Expr::Operator(node) => match node.op.as_str() {
"+" | "-" => {
if node.args.len() != 2 {
return (0, 0);
}
let a = evaluate_index_range(&node.args[0], loop_ranges);
let b = evaluate_index_range(&node.args[1], loop_ranges);
if node.op == "+" {
(a.0 + b.0, a.1 + b.1)
} else {
(a.0 - b.1, a.1 - b.0)
}
}
_ => (0, 0),
},
}
}
#[cfg(test)]
mod evaluability_gate_tests {
use super::*;
use crate::types::Expr;
fn node(op: &str, args: Vec<Expr>) -> Expr {
Expr::operator(ExpressionNode {
op: op.to_string(),
args,
..ExpressionNode::default()
})
}
fn eval_it(expr: &Expr) -> Result<Value, CompileError> {
eval_expression(expr, &HashMap::new(), &[], &[], 0.0)
}
#[test]
fn typo_op_is_rejected_not_nan() {
let err = eval_it(&node("expp", vec![Expr::Number(1.0)]))
.expect_err("a typo'd operator must not evaluate");
assert!(
matches!(err, CompileError::UnloweredOperatorError { ref op } if op == "expp"),
"{err:?}"
);
}
#[test]
fn unlowered_spatial_op_is_rejected_not_nan() {
let err = eval_it(&node("grad", vec![Expr::Variable("c".into())]))
.expect_err("an unlowered spatial operator must not evaluate");
assert!(
matches!(err, CompileError::UnloweredOperatorError { ref op } if op == "grad"),
"{err:?}"
);
}
fn node_with_legal_arity(op: &str) -> Expr {
let arity = crate::op_registry::arity_of(op).expect("registry-legal op");
let n = (0..=3)
.find(|n| arity.admits(*n))
.expect("some arity in 0..=3 is admitted");
node(op, (0..n).map(|_| Expr::Variable("x".into())).collect())
}
#[test]
fn registry_legal_but_unevaluable_ops_are_rejected_not_nan() {
for op in [
"skolem",
"rank",
"distinct",
"argmin",
"argmax",
"ic",
"table_lookup",
] {
assert!(
crate::op_registry::is_core_op(op),
"{op} should be registry-legal, else this test proves nothing"
);
assert!(
!is_evaluable_op(op),
"{op} should have no eval arm, else this test proves nothing"
);
let err = eval_it(&node_with_legal_arity(op)).unwrap_err_or_else_msg(op);
assert!(
matches!(err, CompileError::UnevaluableOperatorError { op: ref got } if got == op),
"{op}: {err:?}"
);
}
}
#[test]
fn unevaluable_op_nested_in_expression_is_rejected() {
let expr = node(
"+",
vec![Expr::Number(1.0), node("skolem", vec![Expr::Number(2.0)])],
);
assert!(
eval_it(&expr).is_err(),
"nested unevaluable op must be caught"
);
}
#[test]
fn evaluable_expression_still_evaluates() {
let expr = node("+", vec![Expr::Number(2.0), Expr::Number(3.0)]);
match eval_it(&expr).expect("a legal expression must evaluate") {
Value::Scalar(s) => assert_eq!(s, 5.0),
Value::Array(_) => panic!("expected a scalar"),
}
}
#[test]
fn every_registry_op_is_either_evaluable_or_gated() {
for op in [
"+",
"-",
"*",
"/",
"^",
"exp",
"log",
"sqrt",
"min",
"max",
"ifelse",
"index",
"faq",
"makearray",
"broadcast",
"reshape",
"transpose",
"concat",
"fn",
"skolem",
"rank",
"distinct",
"argmin",
"argmax",
"ic",
"enum",
"table_lookup",
"apply_expression_template",
"true",
] {
if !crate::op_registry::is_core_op(op) {
continue;
}
if is_evaluable_op(op) {
continue;
}
let err = eval_it(&node_with_legal_arity(op)).unwrap_err_or_else_msg(op);
assert!(
matches!(err, CompileError::UnevaluableOperatorError { .. }),
"{op} is registry-legal and not evaluable, so it must be gated: {err:?}"
);
}
}
#[test]
fn the_true_literal_evaluates_to_one() {
assert!(crate::op_registry::is_core_op("true"), "§4.2 lists `true`");
assert!(is_evaluable_op("true"), "and this evaluator answers for it");
match eval_it(&node("true", Vec::new())).expect("`true` evaluates") {
Value::Scalar(v) => assert_eq!(v, 1.0),
other => panic!("`true` must be the scalar 1.0, got {other:?}"),
}
}
#[test]
fn the_core_minus_evaluable_gap_is_exactly_nine_ops() {
const CORE: &[&str] = &[
"+",
"-",
"*",
"/",
"^",
"neg",
"exp",
"log",
"log10",
"sqrt",
"abs",
"sign",
"floor",
"ceil",
"sin",
"cos",
"tan",
"asin",
"acos",
"atan",
"sinh",
"cosh",
"tanh",
"asinh",
"acosh",
"atanh",
"atan2",
"min",
"max",
"ifelse",
"==",
"!=",
"<",
"<=",
">",
">=",
"and",
"or",
"not",
"D",
"ic",
"Pre",
"const",
"true",
"fn",
"enum",
"table_lookup",
"apply_expression_template",
"faq",
"makearray",
"index",
"broadcast",
"reshape",
"transpose",
"concat",
"skolem",
"rank",
"distinct",
"argmin",
"argmax",
"intersect_polygon",
"polygon_intersection_area",
];
for op in CORE {
assert!(
crate::op_registry::is_core_op(op),
"{op} is listed here but the registry does not carry it"
);
}
let mut gap: Vec<&str> = CORE
.iter()
.copied()
.filter(|op| !is_evaluable_op(op))
.collect();
gap.sort_unstable();
assert_eq!(
gap,
vec![
"apply_expression_template",
"argmax",
"argmin",
"distinct",
"enum",
"ic",
"rank",
"skolem",
"table_lookup",
]
);
}
trait UnwrapErrMsg {
fn unwrap_err_or_else_msg(self, op: &str) -> CompileError;
}
impl UnwrapErrMsg for Result<Value, CompileError> {
fn unwrap_err_or_else_msg(self, op: &str) -> CompileError {
match self {
Ok(v) => panic!("op '{op}' must not evaluate, got {v:?}"),
Err(e) => e,
}
}
}
}
#[cfg(test)]
mod geometry_eval_tests {
use super::*;
use serde_json::json;
fn ring_array(ring: &[(f64, f64)]) -> ArrayD<f64> {
let mut flat = Vec::with_capacity(ring.len() * 2);
for &(lon, lat) in ring {
flat.push(lon);
flat.push(lat);
}
ArrayD::from_shape_vec(IxDyn(&[ring.len(), 2]), flat).unwrap()
}
fn distinct_vertices(ring: &[(f64, f64)]) -> Vec<(f64, f64)> {
match ring.last() {
Some(last) if ring.len() >= 2 && *last == ring[0] => ring[..ring.len() - 1].to_vec(),
_ => ring.to_vec(),
}
}
fn clip_via_evaluator(
src: &[(f64, f64)],
tgt: &[(f64, f64)],
manifold: &str,
) -> Vec<(f64, f64)> {
let mut inputs = HashMap::new();
inputs.insert("src_poly".to_string(), ring_array(src));
inputs.insert("tgt_poly".to_string(), ring_array(tgt));
let node: Expr = serde_json::from_value(json!({
"op": "intersect_polygon",
"id": "overlap_clip",
"manifold": manifold,
"args": ["src_poly", "tgt_poly"],
}))
.unwrap();
match eval_expression(&node, &inputs, &[], &[], 0.0)
.expect("test node is built from evaluable ops")
{
Value::Array(a) => arrayd_to_lonlat(&a).expect("[N,2] ring"),
Value::Scalar(s) => panic!("intersect_polygon evaluated to scalar {s}"),
}
}
fn shoelace_area_faq(ring: &[(f64, f64)]) -> f64 {
let n = ring.len();
if n < 3 {
return 0.0;
}
let next: Vec<(f64, f64)> = (0..n).map(|i| ring[(i + 1) % n]).collect();
let mut inputs = HashMap::new();
inputs.insert("clip".to_string(), ring_array(ring));
inputs.insert("clip_next".to_string(), ring_array(&next));
let agg: Expr = serde_json::from_value(json!({
"op": "faq",
"args": [],
"semiring": "sum_product",
"output_idx": [],
"ranges": { "v": [1, n] },
"expr": {
"op": "*",
"args": [
0.5,
{ "op": "-", "args": [
{ "op": "*", "args": [
{ "op": "index", "args": ["clip", "v", 1] },
{ "op": "index", "args": ["clip_next", "v", 2] }
]},
{ "op": "*", "args": [
{ "op": "index", "args": ["clip_next", "v", 1] },
{ "op": "index", "args": ["clip", "v", 2] }
]}
]}
]
}
}))
.unwrap();
match eval_expression(&agg, &inputs, &[], &[], 0.0)
.expect("test node is built from evaluable ops")
{
Value::Scalar(s) => s.abs(),
Value::Array(_) => panic!("scalar polygon_area FAQ expected"),
}
}
#[test]
fn planar_clip_then_polygon_area_faq_is_exact() {
let src = [(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)];
let tgt = [(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0)];
let ring = clip_via_evaluator(&src, &tgt, "planar");
assert!(ring.len() >= 3, "expected a non-degenerate overlap ring");
let area = shoelace_area_faq(&ring);
assert!(
(area - 1.0).abs() < 1e-9,
"polygon_area FAQ = {area}, expected 1"
);
assert!((area - crate::geometry::shoelace_area(&ring)).abs() < 1e-12);
}
#[test]
fn planar_clip_of_offset_triangles_area_faq() {
let src = [(0.0, 0.0), (4.0, 0.0), (0.0, 4.0)];
let tgt = [(0.0, 0.0), (4.0, 0.0), (4.0, 4.0)];
let ring = clip_via_evaluator(&src, &tgt, "planar");
let area = shoelace_area_faq(&ring);
assert!(
(area - 4.0).abs() < 1e-9,
"polygon_area FAQ = {area}, expected 4"
);
}
#[test]
fn spherical_clip_via_s2_is_nonempty_with_analytic_area() {
let src = [(0.0, 0.0), (90.0, 0.0), (0.0, 90.0)];
let tgt = [(45.0, 0.0), (135.0, 0.0), (45.0, 90.0)];
let ring = clip_via_evaluator(&src, &tgt, "spherical");
assert!(ring.len() >= 3, "the s2 spherical clip should be non-empty");
let area =
crate::geometry::spherical_area(&distinct_vertices(&ring)).expect("spherical area");
assert!(
(area - std::f64::consts::FRAC_PI_4).abs() < 1e-9,
"spherical overlap area = {area}, expected π/4"
);
}
#[test]
fn disjoint_clip_is_empty_ring_with_zero_area_faq() {
let src = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
let tgt = [(5.0, 5.0), (6.0, 5.0), (6.0, 6.0), (5.0, 6.0)];
let ring = clip_via_evaluator(&src, &tgt, "planar");
assert!(ring.is_empty(), "disjoint cells clip to an empty ring");
assert_eq!(shoelace_area_faq(&ring), 0.0);
}
fn fused_area_via_evaluator(src: &[(f64, f64)], tgt: &[(f64, f64)], manifold: &str) -> Value {
let mut inputs = HashMap::new();
inputs.insert("src_poly".to_string(), ring_array(src));
inputs.insert("tgt_poly".to_string(), ring_array(tgt));
let node: Expr = serde_json::from_value(json!({
"op": "polygon_intersection_area",
"manifold": manifold,
"args": ["src_poly", "tgt_poly"],
}))
.unwrap();
eval_expression(&node, &inputs, &[], &[], 0.0)
.expect("test node is built from evaluable ops")
}
#[test]
fn polygon_intersection_area_planar_is_fused_clip_area() {
let src = [(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)];
let tgt = [(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0)];
let area = match fused_area_via_evaluator(&src, &tgt, "planar") {
Value::Scalar(s) => s,
Value::Array(_) => panic!("fused leaf must return a scalar, not a ring"),
};
assert!(
(area - 1.0).abs() < 1e-9,
"polygon_intersection_area = {area}, expected 1"
);
let ring = clip_via_evaluator(&src, &tgt, "planar");
assert!((area - shoelace_area_faq(&ring)).abs() < 1e-12);
}
#[test]
fn polygon_intersection_area_disjoint_is_zero() {
let src = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
let tgt = [(5.0, 5.0), (6.0, 5.0), (6.0, 6.0), (5.0, 6.0)];
match fused_area_via_evaluator(&src, &tgt, "planar") {
Value::Scalar(s) => assert_eq!(s, 0.0, "disjoint overlap area should be 0, got {s}"),
Value::Array(_) => panic!("fused leaf must return a scalar"),
}
}
#[test]
fn polygon_intersection_area_without_manifold_is_unevaluable() {
let mut inputs = HashMap::new();
inputs.insert(
"src_poly".to_string(),
ring_array(&[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)]),
);
inputs.insert(
"tgt_poly".to_string(),
ring_array(&[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0)]),
);
let node: Expr = serde_json::from_value(json!({
"op": "polygon_intersection_area",
"args": ["src_poly", "tgt_poly"],
}))
.unwrap();
match eval_expression(&node, &inputs, &[], &[], 0.0)
.expect("test node is built from evaluable ops")
{
Value::Scalar(s) => assert!(s.is_nan(), "missing manifold should be NaN, got {s}"),
Value::Array(_) => panic!("missing manifold must not produce a scalar area"),
}
}
#[test]
fn intersect_polygon_without_manifold_is_unevaluable() {
let mut inputs = HashMap::new();
inputs.insert(
"src_poly".to_string(),
ring_array(&[(0.0, 0.0), (2.0, 0.0), (2.0, 2.0), (0.0, 2.0)]),
);
inputs.insert(
"tgt_poly".to_string(),
ring_array(&[(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0)]),
);
let node: Expr = serde_json::from_value(json!({
"op": "intersect_polygon",
"args": ["src_poly", "tgt_poly"],
}))
.unwrap();
match eval_expression(&node, &inputs, &[], &[], 0.0)
.expect("test node is built from evaluable ops")
{
Value::Scalar(s) => assert!(s.is_nan(), "missing manifold should be NaN, got {s}"),
Value::Array(_) => panic!("missing manifold must not produce a ring"),
}
}
}
#[cfg(test)]
mod ragged_eval_tests {
use super::*;
use serde_json::json;
fn ragged_sum_node() -> Expr {
let mut agg: Expr = serde_json::from_value(json!({
"op": "faq",
"args": [],
"semiring": "sum_product",
"output_idx": ["i"],
"expr": "k",
"ranges": { "i": [1, 2], "k": [1, 1] }
}))
.unwrap();
if let Some(node) = agg.node_mut() {
node.ranges.as_mut().unwrap().insert(
"k".to_string(),
RangeSpec::RaggedDyn {
offsets: "nedges".into(),
of: vec!["i".into()],
},
);
}
agg
}
fn nedges(values: &[f64]) -> HashMap<String, ArrayD<f64>> {
HashMap::from([(
"nedges".to_string(),
ArrayD::from_shape_vec(IxDyn(&[values.len()]), values.to_vec()).unwrap(),
)])
}
#[test]
fn ragged_contraction_uses_per_parent_dynamic_bound() {
match eval_expression(&ragged_sum_node(), &nedges(&[2.0, 3.0]), &[], &[], 0.0)
.expect("test node is built from evaluable ops")
{
Value::Array(a) => {
assert_eq!(a.shape(), [2]);
assert_eq!(a[IxDyn(&[0])], 3.0);
assert_eq!(a[IxDyn(&[1])], 6.0);
}
Value::Scalar(s) => panic!("expected a [3, 6] array, got scalar {s}"),
}
}
#[test]
fn ragged_empty_segment_yields_additive_identity() {
match eval_expression(&ragged_sum_node(), &nedges(&[0.0, 2.0]), &[], &[], 0.0)
.expect("test node is built from evaluable ops")
{
Value::Array(a) => {
assert_eq!(a[IxDyn(&[0])], 0.0);
assert_eq!(a[IxDyn(&[1])], 3.0);
}
Value::Scalar(s) => panic!("expected a [0, 3] array, got scalar {s}"),
}
}
}
#[cfg(test)]
mod unbound_name_fault_tests {
use super::{lookup_variable, take_const_array_oob};
use crate::faq::empty_derived_extents;
use crate::simulate_array::{ArrMap, ConstArrayScope, EvalEnv};
use ndarray::ArrayD;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
fn fault_for(name: &str, declared: &[&str]) -> String {
let empty_arrays = ArrMap::default();
let rings: RefCell<HashMap<String, ArrayD<f64>>> = RefCell::new(HashMap::new());
let forcing: RefCell<HashMap<String, ArrayD<f64>>> = RefCell::new(HashMap::new());
let declared: HashSet<String> = declared.iter().map(|s| (*s).to_string()).collect();
let env = EvalEnv {
state_arrays: &empty_arrays,
params: &[],
param_names: &[],
t: 0.0,
derived_rings: &rings,
derived_extents: empty_derived_extents(),
forcing: &forcing,
cse: None,
const_lits: None,
const_arrays: ConstArrayScope::empty(),
declared: &declared,
};
let ctx = env.ctx(&empty_arrays);
take_const_array_oob(); let _ = lookup_variable(name, &ctx);
take_const_array_oob().expect("an unresolvable read must latch a fault")
}
#[test]
fn a_declared_name_with_no_value_is_not_reported_as_unbound() {
let msg = fault_for("in_pbl", &["in_pbl", "hpbl"]);
assert!(
msg.contains("E_TREEWALK_UNRESOLVED_ORDER") && msg.contains("in_pbl"),
"a declared name gets the order fault, naming itself: {msg}"
);
assert!(
!msg.contains("E_TREEWALK_UNBOUND_NAME: "),
"and MUST NOT be reported as bound by nothing: {msg}"
);
assert!(
msg.contains("observed_cycle"),
"and points at the check that names the real defect: {msg}"
);
}
#[test]
fn an_undeclared_name_still_reports_unbound() {
let msg = fault_for("undeclaredFloor", &["in_pbl", "hpbl"]);
assert!(
msg.contains("E_TREEWALK_UNBOUND_NAME") && msg.contains("undeclaredFloor"),
"a name nothing declares is still the §5.23 fault: {msg}"
);
}
}
#[cfg(test)]
mod gate_cache_budget_tests {
use super::*;
fn ix(n: i64) -> Option<Rc<crate::broad_phase::OverlapIndex>> {
Some(Rc::new(crate::broad_phase::OverlapIndex::from_owned_pairs(
(1..=n).map(|i| (i, i)).collect(),
)))
}
fn cost(n: i64) -> usize {
ix(n).retained_pairs()
}
type Cache = GateCache<u32, Option<Rc<crate::broad_phase::OverlapIndex>>>;
#[test]
fn an_entry_is_priced_by_what_it_holds_resident_not_by_its_pair_count() {
assert!(
cost(100) > 100,
"pricing an index at len() ignores the run table it also owns"
);
}
#[test]
fn eviction_drops_the_least_recently_used_index_first() {
let u = cost(100);
let mut c: Cache = GateCache::new();
c.insert(1, ix(100));
c.insert(2, ix(100));
assert_eq!(c.pairs, 2 * u);
assert!(c.get(&1).is_some());
c.evict_to_budget(u);
assert!(c.get(&2).is_none(), "the LRU entry survived");
assert!(c.get(&1).is_some(), "the MRU entry was evicted");
assert_eq!(c.pairs, u);
}
#[test]
fn eviction_continues_until_the_cache_is_within_budget() {
let u = cost(100);
let mut c: Cache = GateCache::new();
for k in 0..5 {
c.insert(k, ix(100));
}
assert_eq!(c.pairs, 5 * u);
c.evict_to_budget(2 * u);
assert!(c.pairs <= 2 * u);
assert!(c.get(&3).is_some() && c.get(&4).is_some());
assert!(c.get(&0).is_none() && c.get(&1).is_none() && c.get(&2).is_none());
}
#[test]
fn an_index_a_live_gate_still_holds_is_never_evicted() {
let mut c: Cache = GateCache::new();
let held = ix(100);
c.insert(1, held.clone());
c.evict_to_budget(0);
assert!(
c.get(&1).is_some(),
"evicting an index a live gate holds frees nothing and costs a rebuild"
);
drop(held);
c.evict_to_budget(0);
assert!(c.get(&1).is_none(), "the last reference went and it stayed");
}
#[test]
fn a_declined_gate_costs_no_budget() {
let u = cost(100);
let mut c: Cache = GateCache::new();
c.insert(1, ix(100));
c.insert(2, None);
assert_eq!(c.pairs, u);
c.evict_to_budget(u);
assert!(c.get(&1).is_some());
assert!(matches!(c.get(&2), Some(None)));
}
#[test]
fn a_probe_trims_an_entry_the_budget_no_longer_covers() {
let u = cost(100);
let mut c: Cache = GateCache::new();
c.insert(1, ix(100));
assert_eq!(c.pairs, u);
assert!(
c.get_within_budget(&1, u).is_some(),
"an entry WITHIN budget must survive its own probe"
);
assert!(
c.get_within_budget(&1, u - 1).is_none(),
"the probe kept it"
);
assert_eq!(c.pairs, 0);
}
#[test]
fn a_probe_keeps_an_index_a_live_gate_still_holds() {
let mut c: Cache = GateCache::new();
let held = ix(100);
c.insert(1, held.clone());
assert!(c.get_within_budget(&1, 0).is_some());
drop(held);
assert!(c.get_within_budget(&1, 0).is_none());
}
#[test]
fn re_inserting_a_key_replaces_its_contribution_rather_than_adding_to_it() {
let mut c: Cache = GateCache::new();
c.insert(1, ix(100));
c.insert(1, ix(10));
assert_eq!(c.pairs, cost(10));
}
#[test]
fn a_probe_reprices_an_index_that_grew_after_it_was_cached() {
let held = ix(100);
let mut c: Cache = GateCache::new();
c.insert(1, held.clone());
let at_insert = c.pairs;
let ix = held.as_ref().expect("an index");
assert!(!ix.tgt_adjacency_built());
ix.partners(crate::broad_phase::Side::Tgt, 1);
assert!(ix.tgt_adjacency_built());
assert_eq!(
c.pairs, at_insert,
"nothing has probed the cache, so the price is still the old one"
);
c.get_within_budget(&1, usize::MAX);
assert!(
c.pairs > at_insert,
"the probe must re-read the price: {} did not grow",
c.pairs
);
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod gate_plan_tests {
use crate::broad_phase::{
gate_index_builds, gate_plan_declines, reset_gate_index_builds, reset_gate_plan_declines,
set_gate_plan,
};
use crate::{ProblemOptions, esm_problem, observed_field};
use ndarray::{ArrayD, IxDyn};
use serde_json::{Value, json};
use std::collections::HashMap;
fn arr1(v: &[f64]) -> ArrayD<f64> {
ArrayD::from_shape_vec(IxDyn(&[v.len()]), v.to_vec()).unwrap()
}
fn ix(f: &str, i: &str) -> Value {
json!({"op": "index", "args": [f, i]})
}
const FUELS: usize = 4;
const BINS: usize = 40;
const FACTS: usize = 4_000;
const COUNTIES: usize = 500;
const REACHABLE: usize = BINS * (FACTS / COUNTIES);
const FUEL_MATCHES: usize = FUELS * (BINS / FUELS) * (FACTS / FUELS);
const OVERSHOOT: u128 = (FUEL_MATCHES / REACHABLE) as u128;
struct Tables {
bfuel: Vec<f64>,
ffuel: Vec<f64>,
fcounty: Vec<f64>,
ncounty: Vec<f64>,
weight: Vec<f64>,
}
impl Tables {
fn fixture() -> Tables {
Tables {
bfuel: (0..BINS).map(|i| (i % FUELS) as f64).collect(),
ffuel: (0..FACTS).map(|j| (j % FUELS) as f64).collect(),
fcounty: (0..FACTS).map(|j| (j % COUNTIES) as f64).collect(),
ncounty: vec![7.0],
weight: (1..=FACTS).map(|j| j as f64).collect(),
}
}
fn oracle(&self) -> Vec<f64> {
(0..BINS)
.map(|b| {
let mut acc = 0.0f64;
for f in 0..FACTS {
if self.bfuel[b] == self.ffuel[f] && self.fcounty[f] == self.ncounty[0] {
acc += self.weight[f];
}
}
acc
})
.collect()
}
fn const_arrays(&self) -> HashMap<String, ArrayD<f64>> {
[
("bfuel".to_string(), arr1(&self.bfuel)),
("ffuel".to_string(), arr1(&self.ffuel)),
("fcounty".to_string(), arr1(&self.fcounty)),
("ncounty".to_string(), arr1(&self.ncounty)),
("weight".to_string(), arr1(&self.weight)),
]
.into_iter()
.collect()
}
fn doc(&self, gated: bool) -> Value {
let mut vars = serde_json::Map::new();
for (name, set) in [
("bfuel", "brows"),
("ffuel", "frows"),
("fcounty", "frows"),
("weight", "frows"),
("ncounty", "nrows"),
] {
vars.insert(name.into(), json!({"type": "parameter", "shape": [set]}));
}
vars.insert("E".into(), json!({"type": "unknown", "shape": ["brows"]}));
let mut node = json!({
"op": "faq",
"reduce": "+",
"output_idx": ["b"],
"ranges": {
"b": {"from": "brows"},
"f": {"from": "frows"},
"n": {"from": "nrows"}
},
"args": ["bfuel", "ffuel", "fcounty", "ncounty", "weight"],
"expr": ix("weight", "f")
});
let obj = node.as_object_mut().unwrap();
if gated {
obj.insert(
"join".into(),
json!([
{"on": [["bfuel", "ffuel"]]},
{"on": [["fcounty", "ncounty"]]}
]),
);
} else {
obj.insert(
"filter".into(),
json!({"op": "and", "args": [
{"op": "==", "args": [ix("bfuel", "b"), ix("ffuel", "f")]},
{"op": "==", "args": [ix("fcounty", "f"), ix("ncounty", "n")]}
]}),
);
}
json!({
"esm": "1.1.0",
"metadata": {"name": "join_gate_plan"},
"index_sets": {
"brows": {"kind": "interval", "size": BINS},
"frows": {"kind": "interval", "size": FACTS},
"nrows": {"kind": "interval", "size": 1}
},
"models": {"J": {
"variables": Value::Object(vars),
"equations": [{"lhs": "E", "rhs": node}]
}}
})
}
}
fn prepare(doc: &Value, t: &Tables) -> crate::EsmProblem {
esm_problem(
doc,
(0.0, 0.0),
ProblemOptions {
model_name: Some("J".into()),
const_arrays: t.const_arrays(),
build_providers: Vec::new(),
compiler: Some(crate::Compiler::Interpreter),
..Default::default()
},
)
.expect("prepare")
}
fn run(t: &Tables, gated: bool) -> (Vec<f64>, u64, u64) {
let doc = t.doc(gated);
reset_gate_plan_declines();
reset_gate_index_builds();
let prep = prepare(&doc, t);
let (declines, builds) = (gate_plan_declines(), gate_index_builds());
let field = observed_field(&prep, "E").expect("E materialized");
(field.iter().copied().collect(), declines, builds)
}
fn run_planned(t: &Tables, ratio: u128, floor: usize) -> (Vec<f64>, u64, u64) {
let (pr, pf) = set_gate_plan(ratio, floor);
let out = run(t, true);
set_gate_plan(pr, pf);
out
}
#[test]
fn a_declined_gate_changes_no_answer() {
let t = Tables::fixture();
let oracle = t.oracle();
assert!(
oracle.iter().any(|v| *v > 0.0),
"the fixture must actually match something"
);
let (filtered, filter_declines, _) = run(&t, false);
assert_eq!(
filter_declines, 0,
"the un-gated arm resolves no gate, so it can decline none"
);
let (planned, planned_declines, _) = run_planned(&t, 0, 0);
let (unplanned, unplanned_declines, _) = run_planned(&t, u128::MAX, usize::MAX);
assert_eq!(
planned_declines, 1,
"the redundant fuel gate should be declined, and only it"
);
assert_eq!(
unplanned_declines, 0,
"a floor out of reach must decline nothing"
);
assert_eq!(
planned, unplanned,
"declining a gate changed an answer; it may only change cost"
);
assert_eq!(
planned, filtered,
"the planned arm and the hand-written filter must agree bit for bit"
);
assert_eq!(
planned, oracle,
"neither arm reproduces the plain-Rust oracle"
);
}
#[test]
fn the_ratio_is_weighed_against_what_a_sibling_gate_left_reachable() {
assert_eq!(OVERSHOOT, 125, "the fixture's overshoot moved");
let t = Tables::fixture();
let oracle = t.oracle();
let (just_under, declined, _) = run_planned(&t, OVERSHOOT - 1, 1);
let (exactly, kept, _) = run_planned(&t, OVERSHOOT, 1);
assert_eq!(
declined, 1,
"a ratio just under the overshoot must decline the redundant gate"
);
assert_eq!(
kept, 0,
"a ratio at the overshoot must keep it: the test is `>`, not `>=`"
);
let (floored, floored_declines, _) = run_planned(&t, OVERSHOOT - 1, FUEL_MATCHES + 1);
assert_eq!(
floored_declines, 0,
"a gate below the floor must be kept however uneconomic it looks"
);
assert_eq!(just_under, oracle, "the declining arm lost the answer");
assert_eq!(exactly, oracle, "the keeping arm lost the answer");
assert_eq!(floored, oracle, "the floored arm lost the answer");
}
#[test]
fn a_gate_with_no_cheaper_sibling_is_kept() {
let t = Tables::fixture();
let mut doc = t.doc(true);
doc["models"]["J"]["equations"][0]["rhs"]["join"] = json!([{"on": [["bfuel", "ffuel"]]}]);
doc["models"]["J"]["equations"][0]["rhs"]["ranges"]
.as_object_mut()
.unwrap()
.remove("n");
let (pr, pf) = set_gate_plan(0, 0);
reset_gate_plan_declines();
reset_gate_index_builds();
let prep = prepare(&doc, &t);
let (declines, builds) = (gate_plan_declines(), gate_index_builds());
set_gate_plan(pr, pf);
assert_eq!(
declines, 0,
"the only gate in a node must never be declined, whatever the ratio"
);
assert!(
builds >= 1,
"no index was built: the node is ungated, not kept"
);
let field = observed_field(&prep, "E").expect("E materialized");
let got: Vec<f64> = field.iter().copied().collect();
let want: Vec<f64> = (0..BINS)
.map(|b| {
(0..FACTS)
.filter(|&f| t.bfuel[b] == t.ffuel[f])
.map(|f| t.weight[f])
.sum()
})
.collect();
assert_eq!(got, want);
}
}