use super::*;
use crate::types::{Expr, ExpressionNode};
use rustc_hash::{FxHashMap, FxHashSet};
use std::cell::RefCell;
pub(super) const BOX_TRANSPARENT_OPS: &[&str] = &[
"+",
"-",
"*",
"/",
"^",
"pow",
"min",
"max",
"atan2",
"and",
"or",
"neg",
"index",
"==",
"!=",
"<",
"<=",
">",
">=",
"ifelse",
"exp",
"log",
"log10",
"sqrt",
"abs",
"sign",
"floor",
"ceil",
"sin",
"cos",
"tan",
"asin",
"acos",
"atan",
"sinh",
"cosh",
"tanh",
"asinh",
"acosh",
"atanh",
"not",
"broadcast",
];
#[derive(PartialEq, Eq, Hash)]
enum CseKey {
Int(i64),
Num(u64),
Var(u32),
Op {
attrs: u32,
kids: SmallVec<[u32; 4]>,
},
}
const PURE_BIT: u32 = 1 << 31;
const NOT_MEMO: u32 = u32::MAX;
#[derive(Default)]
struct ClassTable {
keys: FxHashMap<CseKey, u32>,
next_class: u32,
strs: FxHashMap<Box<str>, u32>,
memoizable: FxHashMap<usize, u32>,
pending: Vec<(usize, u32)>,
analysed_roots: FxHashSet<usize>,
counts: FxHashMap<(u32, u32), u32>,
seen: Vec<(usize, u32, u32)>,
pure_cand: FxHashMap<usize, u32>,
pure_binders: FxHashMap<u32, u64>,
next_static_scope: u32,
}
fn binder_names_hash<'a>(
a: impl Iterator<Item = &'a str>,
b: impl Iterator<Item = &'a str>,
) -> u64 {
use std::hash::{Hash, Hasher};
let mut names: SmallVec<[&str; 8]> = a.chain(b).collect();
names.sort_unstable();
names.dedup();
let mut h = rustc_hash::FxHasher::default();
names.len().hash(&mut h);
for n in names {
n.hash(&mut h);
}
h.finish()
}
impl ClassTable {
fn analyse(
&mut self,
root: &Expr,
idx_binders: &[String],
c_binders: &[String],
consts: &FxHashSet<String>,
const_arrays: &FxHashSet<String>,
) {
let key = root as *const Expr as usize;
if !self.analysed_roots.insert(key) {
return;
}
if self.memoizable.get(&key).is_some_and(|&c| c != NOT_MEMO) {
self.memoizable.insert(key, NOT_MEMO);
self.pending.push((key, NOT_MEMO));
}
self.counts.clear();
self.seen.clear();
self.pure_cand.clear();
let scope = self.new_static_scope();
let binders = Binders {
idx: idx_binders,
c: c_binders,
consts,
const_arrays,
};
self.walk(root, scope, true, Some(&binders));
for &(addr, scope, class) in &std::mem::take(&mut self.seen) {
if !self.analysed_roots.contains(&addr)
&& self.counts.get(&(scope, class)).copied().unwrap_or(0) >= 2
&& self.memoizable.insert(addr, class) != Some(class)
{
self.pending.push((addr, class));
}
}
let pure_ctx = binder_names_hash(
idx_binders.iter().map(String::as_str),
c_binders.iter().map(String::as_str),
);
for (addr, class) in std::mem::take(&mut self.pure_cand) {
if !self.analysed_roots.contains(&addr) {
self.pure_binders.insert(class, pure_ctx);
if self.memoizable.insert(addr, class | PURE_BIT) != Some(class | PURE_BIT) {
self.pending.push((addr, class | PURE_BIT));
}
}
}
}
fn new_static_scope(&mut self) -> u32 {
self.next_static_scope += 1;
self.next_static_scope
}
fn intern_str(&mut self, s: &str) -> u32 {
if let Some(&i) = self.strs.get(s) {
return i;
}
let n = self.strs.len() as u32;
self.strs.insert(s.into(), n);
n
}
fn intern(&mut self, key: CseKey) -> u32 {
let next = self.next_class;
match self.keys.entry(key) {
std::collections::hash_map::Entry::Occupied(e) => *e.get(),
std::collections::hash_map::Entry::Vacant(e) => {
assert!(next + 1 < PURE_BIT, "CSE class id space exhausted");
e.insert(next);
self.next_class += 1;
next
}
}
}
fn walk(
&mut self,
e: &Expr,
scope: u32,
evaluated: bool,
binders: Option<&Binders<'_>>,
) -> Walked {
let mut pure = true;
let mut uses_idx = false;
let class = match e {
Expr::Integer(i) => self.intern(CseKey::Int(*i)),
Expr::Number(n) => self.intern(CseKey::Num(n.to_bits())),
Expr::Variable(v) => {
match binders {
Some(b) if v != "t" && b.is_idx(v) => uses_idx = true,
Some(b) if v != "t" && b.is_const(v) => {
uses_idx |= b.const_arrays.contains(v);
}
_ => {
pure = false;
}
}
let id = self.intern_str(v);
self.intern(CseKey::Var(id))
}
Expr::Operator(node) => {
let transparent = BOX_TRANSPARENT_OPS.contains(&node.op.as_str());
let opener = matches!(node.op.as_str(), "faq" | "makearray");
let child_scope = if transparent {
scope
} else {
self.new_static_scope()
};
let attrs = if transparent || opener {
self.attrs_id(node)
} else {
None
};
let kids_evaluated = evaluated && (transparent || opener);
let child_binders = if transparent { binders } else { None };
let mut kids: SmallVec<[u32; 4]> = SmallVec::new();
let mut ok = attrs.is_some();
let mut kid_addrs: SmallVec<[usize; 4]> = SmallVec::new();
pure = transparent && attrs.is_some();
if node.op == "index" {
for (i, c) in node.args.iter().enumerate() {
let w = self.walk(c, child_scope, kids_evaluated && i == 0, child_binders);
kid_addrs.push(c as *const Expr as usize);
pure &= w.pure;
uses_idx |= w.uses_idx;
match w.class {
Some(k) => kids.push(k),
None => ok = false,
}
}
} else {
node.for_each_child(&mut |c| {
let w = self.walk(c, child_scope, kids_evaluated, child_binders);
kid_addrs.push(c as *const Expr as usize);
pure &= w.pure;
uses_idx |= w.uses_idx;
match w.class {
Some(k) => kids.push(k),
None => ok = false,
}
});
}
if !ok {
return Walked::unclassified();
}
if pure {
for a in &kid_addrs {
self.pure_cand.remove(a);
}
}
self.intern(CseKey::Op {
attrs: attrs.expect("checked above"),
kids,
})
}
};
if evaluated {
let addr = e as *const Expr as usize;
*self.counts.entry((scope, class)).or_insert(0) += 1;
self.seen.push((addr, scope, class));
if pure && uses_idx {
self.pure_cand.insert(addr, class);
}
}
Walked {
class: Some(class),
pure,
uses_idx,
}
}
}
struct Binders<'a> {
idx: &'a [String],
c: &'a [String],
consts: &'a FxHashSet<String>,
const_arrays: &'a FxHashSet<String>,
}
impl Binders<'_> {
fn is_idx(&self, name: &str) -> bool {
self.idx.iter().any(|s| s == name) || self.c.iter().any(|s| s == name)
}
fn is_const(&self, name: &str) -> bool {
self.is_idx(name) || self.consts.contains(name)
}
}
#[derive(Clone, Copy)]
struct Walked {
class: Option<u32>,
pure: bool,
uses_idx: bool,
}
impl Walked {
fn unclassified() -> Walked {
Walked {
class: None,
pure: false,
uses_idx: false,
}
}
}
impl ClassTable {
fn attrs_id(&mut self, node: &ExpressionNode) -> Option<u32> {
let rendered = attrs_key(node)?;
Some(match rendered {
Rendered::Bare => self.intern_str(&node.op),
Rendered::Blob(s) => self.intern_str(&s),
})
}
}
enum Rendered {
Bare,
Blob(String),
}
fn attrs_key(node: &ExpressionNode) -> Option<Rendered> {
if node.join.is_some()
|| node.axes.is_some()
|| node.bindings.is_some()
|| node.key.is_some()
|| node.table.is_some()
|| node.output.is_some()
|| node.manifold.is_some()
|| node.arg.is_some()
|| node.int_var.is_some()
{
return None;
}
let bare = node.wrt.is_none()
&& node.dim.is_none()
&& node.output_idx.is_none()
&& node.ranges.is_none()
&& node.reduce.is_none()
&& node.semiring.is_none()
&& node.regions.is_none()
&& node.shape.is_none()
&& node.perm.is_none()
&& node.axis.is_none()
&& node.broadcast_fn.is_none()
&& node.name.is_none()
&& node.label.is_none()
&& node.value.is_none()
&& node.id.is_none();
if bare {
return Some(Rendered::Bare);
}
let mut s = String::with_capacity(node.op.len() + 64);
s.push_str(&node.op);
use std::fmt::Write as _;
let _ = write!(
s,
"|w={:?}|d={:?}|oi={:?}|rd={:?}|sr={:?}|rg={:?}|sh={:?}|pm={:?}|ax={:?}|bf={:?}|nm={:?}|lb={:?}|id={:?}",
node.wrt,
node.dim,
node.output_idx,
node.reduce,
node.semiring,
node.regions,
node.shape,
node.perm,
node.axis,
node.broadcast_fn,
node.name,
node.label,
node.id,
);
if let Some(r) = &node.ranges {
let mut ks: Vec<&String> = r.keys().collect();
ks.sort_unstable();
s.push_str("|rn=");
for k in ks {
let spec = serde_json::to_string(&r[k]).ok()?;
let _ = write!(s, "{k}={spec};");
}
}
if let Some(v) = &node.value {
s.push_str("|v=");
s.push_str(&serde_json::to_string(v).ok()?);
}
Some(Rendered::Blob(s))
}
const EMPTY_KEY: u64 = u64::MAX;
const ADDR_SHIFT: u32 = std::mem::align_of::<Expr>().trailing_zeros();
#[derive(Default)]
struct AddrClasses {
slots: Box<[(u64, u32)]>,
count: usize,
}
impl AddrClasses {
#[inline(always)]
fn bucket(key: u64, len: usize) -> usize {
let h = key.wrapping_mul(0x9E37_79B9_7F4A_7C15);
(((h as u128) * (len as u128)) >> 64) as usize
}
#[inline]
fn get(&self, addr: usize) -> Option<u32> {
let len = self.slots.len();
if len == 0 {
return None;
}
let key = (addr >> ADDR_SHIFT) as u64;
let mut i = Self::bucket(key, len);
loop {
let (k, class) = self.slots[i];
if k == key {
return Some(class);
}
if k == EMPTY_KEY {
return None;
}
i += 1;
if i == len {
i = 0;
}
}
}
fn add(&mut self, entries: &[(usize, u32)]) {
if entries.is_empty() {
return;
}
let want = self.count + entries.len();
if self.slots.len() * 7 < want * 10 {
let cap = (want * 10 / 7 + 16).max(self.slots.len() * 2);
let carried: Vec<(u64, u32)> = self
.slots
.iter()
.filter(|(k, _)| *k != EMPTY_KEY)
.copied()
.collect();
self.slots = vec![(EMPTY_KEY, 0u32); cap].into_boxed_slice();
self.count = 0;
for (key, class) in carried {
self.insert(key, class);
}
}
for &(addr, class) in entries {
self.insert((addr >> ADDR_SHIFT) as u64, class);
}
}
fn insert(&mut self, key: u64, class: u32) {
let len = self.slots.len();
let mut i = Self::bucket(key, len);
loop {
if self.slots[i].0 == EMPTY_KEY {
self.slots[i] = (key, class);
self.count += 1;
return;
}
if self.slots[i].0 == key {
self.slots[i].1 = class;
return;
}
i += 1;
if i == len {
i = 0;
}
}
}
}
struct Slab {
data: ArrayD<f64>,
origin: DimI,
}
impl Slab {
fn empty() -> Slab {
Slab {
data: ArrayD::from_shape_vec(IxDyn(&[0]), Vec::new()).expect("0-length array"),
origin: DimI::new(),
}
}
}
#[derive(Clone, Copy)]
enum Slot {
Scalar(f64),
Arr(usize),
}
#[derive(Clone, Copy)]
struct MemoCell {
stamp: u64,
slot: Slot,
}
impl Default for MemoCell {
fn default() -> Self {
MemoCell {
stamp: 0,
slot: Slot::Scalar(0.0),
}
}
}
fn box_sig(bx: &VecBox) -> u64 {
use std::hash::{Hash, Hasher};
let mut h = rustc_hash::FxHasher::default();
bx.syms.len().hash(&mut h);
for s in bx.syms {
s.hash(&mut h);
}
bx.lo.hash(&mut h);
bx.shape.hash(&mut h);
bx.cnames.len().hash(&mut h);
for s in bx.cnames {
s.hash(&mut h);
}
bx.cvals.hash(&mut h);
h.finish()
}
const PURE_MAX_ENTRIES: usize = 4096;
const PURE_MAX_ELEMS: usize = 8 << 20;
#[derive(Default)]
struct Inner {
classes: ClassTable,
addrs: AddrClasses,
memo: Vec<Vec<MemoCell>>,
#[allow(clippy::vec_box)]
slabs: Vec<Box<Slab>>,
used: usize,
pmemo: FxHashMap<(u64, u32), Slot>,
#[allow(clippy::vec_box)]
pslabs: Vec<Box<Slab>>,
pelems: usize,
pgen: u64,
pgen_set: bool,
const_names: FxHashSet<String>,
const_arrays: FxHashSet<String>,
const_names_set: bool,
tag: u64,
depth: u32,
next_scope: u64,
scope: u64,
scope_stack: SmallVec<[u64; 8]>,
}
impl Inner {
fn clear_persistent(&mut self) {
self.pmemo.clear();
self.pslabs.clear();
self.pelems = 0;
}
}
#[derive(Default)]
pub(super) struct CseRt {
inner: RefCell<Inner>,
}
impl CseRt {
pub(super) fn retarget(&self, tag: u64) {
let mut i = self.inner.borrow_mut();
if i.tag != tag {
i.tag = tag;
i.classes = ClassTable::default();
i.addrs = AddrClasses::default();
i.memo.clear();
i.clear_persistent();
i.const_names = FxHashSet::default();
i.const_arrays = FxHashSet::default();
i.const_names_set = false;
}
}
pub(super) fn bind_params(&self, params: &[f64]) {
use std::hash::{Hash, Hasher};
let mut h = rustc_hash::FxHasher::default();
params.len().hash(&mut h);
for p in params {
p.to_bits().hash(&mut h);
}
let g = h.finish();
let mut i = self.inner.borrow_mut();
if !i.pgen_set || i.pgen != g {
i.pgen = g;
i.pgen_set = true;
i.clear_persistent();
}
}
pub(super) fn invalidate_consts(&self) {
let mut i = self.inner.borrow_mut();
i.clear_persistent();
i.const_names = FxHashSet::default();
i.const_arrays = FxHashSet::default();
i.const_names_set = false;
}
pub(super) fn set_const_names(
&self,
build: impl FnOnce() -> (FxHashSet<String>, FxHashSet<String>),
) {
let mut i = self.inner.borrow_mut();
if !i.const_names_set {
i.const_names_set = true;
let (all, arrays) = build();
i.const_names = all;
i.const_arrays = arrays;
}
}
pub(super) fn analyse(&self, body: &Expr, idx_binders: &[String], c_binders: &[String]) {
let mut i = self.inner.borrow_mut();
{
let Inner {
classes,
const_names,
const_arrays,
..
} = &mut *i;
classes.analyse(body, idx_binders, c_binders, const_names, const_arrays);
}
if i.classes.pending.is_empty() {
return;
}
let pending = std::mem::take(&mut i.classes.pending);
i.addrs.add(&pending);
}
pub(super) fn scope(&self, pool: &mut Pool) -> CseScope<'_> {
self.enter_scope(pool);
CseScope { rt: self }
}
fn enter_scope(&self, pool: &mut Pool) {
let mut i = self.inner.borrow_mut();
if i.depth == 0 {
for idx in 0..i.used {
let old = std::mem::replace(&mut *i.slabs[idx], Slab::empty());
pool.give_array(old.data);
}
i.used = 0;
i.scope_stack.clear();
i.scope = 0;
}
i.depth += 1;
i.next_scope += 1;
let s = i.next_scope;
let prev = i.scope;
i.scope_stack.push(prev);
i.scope = s;
let level = i.depth as usize - 1;
let classes = i.classes.next_class as usize;
if i.memo.len() <= level {
i.memo.resize_with(level + 1, Vec::new);
}
if i.memo[level].len() < classes {
i.memo[level].resize_with(classes, MemoCell::default);
}
}
fn exit_scope(&self) {
let mut i = self.inner.borrow_mut();
i.scope = i.scope_stack.pop().unwrap_or(0);
i.depth = i.depth.saturating_sub(1);
}
#[inline]
pub(super) fn class_of(&self, expr: &Expr) -> Option<u32> {
let i = self.inner.borrow();
if i.depth == 0 {
return None;
}
let addr = expr as *const Expr as usize;
i.addrs.get(addr).filter(|&c| c != NOT_MEMO)
}
fn slab_view<'a>(&'a self, i: &Inner, idx: usize) -> VecValue<'a> {
let slab: &Slab = &i.slabs[idx];
let origin = slab.origin.clone();
let ptr: *const ArrayD<f64> = &slab.data;
VecValue::View {
data: unsafe { (*ptr).view() },
origin,
}
}
fn pslab_view<'a>(&'a self, i: &Inner, idx: usize) -> VecValue<'a> {
let slab: &Slab = &i.pslabs[idx];
let origin = slab.origin.clone();
let ptr: *const ArrayD<f64> = &slab.data;
VecValue::View {
data: unsafe { (*ptr).view() },
origin,
}
}
pub(super) fn get<'a>(&'a self, class: u32, bx: &VecBox) -> Option<VecValue<'a>> {
let i = self.inner.borrow();
if class & PURE_BIT != 0 {
if !Self::pure_ctx_ok(&i, class, bx) {
return None;
}
let slot = *i.pmemo.get(&(box_sig(bx), class & !PURE_BIT))?;
return match slot {
Slot::Scalar(s) => Some(VecValue::Scalar(s)),
Slot::Arr(idx) => Some(self.pslab_view(&i, idx)),
};
}
let level = (i.depth as usize).checked_sub(1)?;
let cell = *i.memo.get(level)?.get(class as usize)?;
if cell.stamp != i.scope {
return None;
}
match cell.slot {
Slot::Scalar(s) => Some(VecValue::Scalar(s)),
Slot::Arr(idx) => Some(self.slab_view(&i, idx)),
}
}
fn store(&self, class: u32, slot: Slot) {
debug_assert!(class & PURE_BIT == 0, "PURE class reached the dense memo");
if class & PURE_BIT != 0 {
return;
}
let mut i = self.inner.borrow_mut();
let Some(level) = (i.depth as usize).checked_sub(1) else {
return;
};
let stamp = i.scope;
if i.memo.len() <= level {
i.memo.resize_with(level + 1, Vec::new);
}
let row = &mut i.memo[level];
if row.len() <= class as usize {
row.resize_with(class as usize + 1, MemoCell::default);
}
row[class as usize] = MemoCell { stamp, slot };
}
pub(super) fn put<'a>(&'a self, class: u32, v: VecValue<'a>, bx: &VecBox) -> VecValue<'a> {
if class & PURE_BIT != 0 {
return self.pput(class, v, bx);
}
match v {
VecValue::Scalar(s) => {
self.store(class, Slot::Scalar(s));
VecValue::Scalar(s)
}
VecValue::View { data, origin } => VecValue::View { data, origin },
VecValue::Owned { data, origin } => {
let idx = {
let mut i = self.inner.borrow_mut();
let idx = i.used;
if idx == i.slabs.len() {
i.slabs.push(Box::new(Slab::empty()));
}
*i.slabs[idx] = Slab { data, origin };
i.used += 1;
idx
};
self.store(class, Slot::Arr(idx));
let i = self.inner.borrow();
self.slab_view(&i, idx)
}
}
}
fn pure_ctx_ok(i: &Inner, class: u32, bx: &VecBox) -> bool {
i.classes
.pure_binders
.get(&(class & !PURE_BIT))
.is_some_and(|&ctx| {
ctx == binder_names_hash(
bx.syms.iter().map(String::as_str),
bx.cnames.iter().map(String::as_str),
)
})
}
fn pput<'a>(&'a self, class: u32, v: VecValue<'a>, bx: &VecBox) -> VecValue<'a> {
{
let i = self.inner.borrow();
if !Self::pure_ctx_ok(&i, class, bx) {
return v;
}
}
let key = (box_sig(bx), class & !PURE_BIT);
match v {
VecValue::Scalar(s) => {
self.inner.borrow_mut().pmemo.insert(key, Slot::Scalar(s));
VecValue::Scalar(s)
}
VecValue::View { data, origin } => VecValue::View { data, origin },
VecValue::Owned { data, origin } => {
let n = data.len();
{
let i = self.inner.borrow();
if i.pslabs.len() >= PURE_MAX_ENTRIES || i.pelems + n > PURE_MAX_ELEMS {
return VecValue::Owned { data, origin };
}
}
let idx = {
let mut i = self.inner.borrow_mut();
i.pelems += n;
i.pslabs.push(Box::new(Slab { data, origin }));
let idx = i.pslabs.len() - 1;
i.pmemo.insert(key, Slot::Arr(idx));
idx
};
let i = self.inner.borrow();
self.pslab_view(&i, idx)
}
}
}
}
pub(super) struct CseScope<'r> {
rt: &'r CseRt,
}
impl Drop for CseScope<'_> {
fn drop(&mut self) {
self.rt.exit_scope();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ExpressionNode;
fn op(name: &str, args: Vec<Expr>) -> Expr {
Expr::operator(ExpressionNode {
op: name.to_string(),
args,
..Default::default()
})
}
fn nobox() -> VecBox<'static> {
VecBox {
syms: &[],
lo: &[],
shape: &[],
cnames: &[],
cvals: &[],
}
}
fn analyse_plain(t: &mut ClassTable, body: &Expr) {
t.analyse(body, &[], &[], &FxHashSet::default(), &FxHashSet::default());
}
#[test]
fn repeated_sibling_is_memoizable_singleton_is_not() {
let dup = || {
op(
"-",
vec![Expr::Variable("a".into()), Expr::Variable("b".into())],
)
};
let body = op(
"+",
vec![op("*", vec![dup(), dup()]), Expr::Variable("c".into())],
);
let mut t = ClassTable::default();
analyse_plain(&mut t, &body);
let Expr::Operator(root) = &body else {
unreachable!()
};
let Expr::Operator(mul) = &root.args[0] else {
unreachable!()
};
let l = &mul.args[0] as *const Expr as usize;
let r = &mul.args[1] as *const Expr as usize;
assert_eq!(
t.memoizable.get(&l).copied(),
t.memoizable.get(&r).copied(),
"identical siblings must share a class"
);
assert!(t.memoizable.contains_key(&l), "the repeat is memoizable");
let root_addr = &body as *const Expr as usize;
assert!(
!t.memoizable.contains_key(&root_addr),
"a singleton (the root) must not be memoized"
);
}
#[test]
fn repeat_across_a_binder_is_not_shared() {
let inner = op(
"-",
vec![Expr::Variable("a".into()), Expr::Variable("b".into())],
);
let agg = Expr::operator(ExpressionNode {
op: "faq".to_string(),
args: vec![],
output_idx: Some(vec!["i".to_string()]),
expr: Some(Box::new(inner)),
..Default::default()
});
let outer = op(
"-",
vec![Expr::Variable("a".into()), Expr::Variable("b".into())],
);
let body = op("+", vec![agg, outer]);
let mut t = ClassTable::default();
analyse_plain(&mut t, &body);
let Expr::Operator(root) = &body else {
unreachable!()
};
let Expr::Operator(agg_n) = &root.args[0] else {
unreachable!()
};
let inner_addr = agg_n.expr.as_deref().expect("body") as *const Expr as usize;
let outer_addr = &root.args[1] as *const Expr as usize;
assert!(
!t.memoizable.contains_key(&inner_addr) && !t.memoizable.contains_key(&outer_addr),
"occurrences in different scopes must not be marked shareable"
);
}
#[test]
fn unknown_operator_blocks_classification() {
let weird = || {
Expr::operator(ExpressionNode {
op: "reshape".to_string(),
args: vec![Expr::Variable("a".into())],
shape: Some(vec![2, 2]),
..Default::default()
})
};
let body = op("+", vec![weird(), weird()]);
let mut t = ClassTable::default();
analyse_plain(&mut t, &body);
let Expr::Operator(root) = &body else {
unreachable!()
};
for i in 0..2 {
let a = &root.args[i] as *const Expr as usize;
assert!(!t.memoizable.contains_key(&a), "unknown op stays unshared");
}
}
#[test]
fn signed_zero_literals_are_distinct_classes() {
let mut t = ClassTable::default();
let a = t.intern(CseKey::Num(0.0f64.to_bits()));
let b = t.intern(CseKey::Num((-0.0f64).to_bits()));
assert_ne!(a, b);
}
fn repeats_body(k: usize) -> Expr {
let a = format!("a{k}");
let dup = || {
op(
"-",
vec![Expr::Variable(a.clone()), Expr::Variable("b".into())],
)
};
let sq = || op("*", vec![dup(), dup()]);
op(
"+",
vec![
op("*", vec![sq(), sq()]),
op("min", vec![op("abs", vec![dup()]), sq()]),
Expr::Variable("c".into()),
],
)
}
#[test]
fn the_resolved_table_agrees_with_the_map() {
let bodies: Vec<Expr> = (0..24).map(repeats_body).collect();
let rt = CseRt::default();
for b in &bodies {
rt.analyse(b, &[], &[]);
}
let i = rt.inner.borrow();
assert!(!i.classes.memoizable.is_empty(), "something was classified");
assert_eq!(
i.addrs.count,
i.classes.memoizable.len(),
"every memoizable address is in the resolved table exactly once"
);
for (&addr, &class) in &i.classes.memoizable {
assert_eq!(
i.addrs.get(addr),
Some(class),
"resolved table disagrees at {addr:#x}"
);
}
for b in &bodies {
let root = b as *const Expr as usize;
assert!(!i.classes.memoizable.contains_key(&root));
assert_eq!(i.addrs.get(root), None, "the root must not resolve");
}
}
#[test]
fn retarget_discards_the_resolved_table() {
let body = repeats_body(0);
let Expr::Operator(root) = &body else {
unreachable!()
};
let Expr::Operator(mul) = &root.args[0] else {
unreachable!()
};
let shared = &mul.args[0];
let rt = CseRt::default();
let mut pool = Pool::default();
rt.retarget(1);
rt.analyse(&body, &[], &[]);
{
let _s = rt.scope(&mut pool);
assert!(rt.class_of(shared).is_some(), "the repeat is classified");
}
rt.retarget(2);
{
let _s = rt.scope(&mut pool);
assert!(
rt.class_of(shared).is_none(),
"a different rule set must invalidate the resolved table"
);
}
rt.analyse(&body, &[], &[]);
{
let _s = rt.scope(&mut pool);
assert!(
rt.class_of(shared).is_some(),
"re-analysis under the new tag repopulates it"
);
}
}
#[test]
fn the_resolved_table_spans_distant_regions() {
let mut t = AddrClasses::default();
t.add(&[(0x5000_0000_1000, 7), (0x5000_0000_2000, 9)]);
assert_eq!(t.get(0x5000_0000_1000), Some(7));
assert_eq!(t.get(0x5000_0000_2000), Some(9));
assert_eq!(t.get(0x5000_0000_3000), None);
t.add(&[(0x1000, 11)]);
assert_eq!(t.get(0x1000), Some(11));
assert_eq!(t.get(0x5000_0000_1000), Some(7));
assert_eq!(t.get(0x5000_0000_2000), Some(9));
}
#[test]
fn the_resolved_table_round_trips_a_dense_run() {
let base = 0x7f00_0000_0000usize;
let stride = std::mem::size_of::<Expr>();
let entries: Vec<(usize, u32)> = (0..5000).map(|k| (base + k * stride, k as u32)).collect();
let mut t = AddrClasses::default();
for chunk in entries.chunks(97) {
t.add(chunk);
}
assert_eq!(t.count, entries.len());
for &(addr, class) in &entries {
assert_eq!(t.get(addr), Some(class));
}
for k in 0..5000usize {
assert_eq!(t.get(base + k * stride + stride / 2 + 8), None);
}
assert_eq!(t.get(base - stride), None);
assert_eq!(t.get(base + 5000 * stride), None);
}
#[test]
fn a_nested_scope_does_not_evict_the_enclosing_one() {
let rt = CseRt::default();
let mut pool = Pool::default();
let outer = rt.scope(&mut pool);
rt.put(3, VecValue::Scalar(1.0), &nobox());
{
let _inner = rt.scope(&mut pool);
assert!(rt.get(3, &nobox()).is_none(), "a fresh scope starts empty");
rt.put(3, VecValue::Scalar(2.0), &nobox());
assert!(matches!(rt.get(3, &nobox()), Some(VecValue::Scalar(v)) if v == 2.0));
}
assert!(
matches!(rt.get(3, &nobox()), Some(VecValue::Scalar(v)) if v == 1.0),
"the enclosing scope's entry must survive the nested one"
);
drop(outer);
}
#[test]
fn sibling_scopes_do_not_share() {
let rt = CseRt::default();
let mut pool = Pool::default();
let outer = rt.scope(&mut pool);
{
let _a = rt.scope(&mut pool);
rt.put(5, VecValue::Scalar(1.0), &nobox());
}
{
let _b = rt.scope(&mut pool);
assert!(
rt.get(5, &nobox()).is_none(),
"a sibling scope must not see the entry"
);
}
drop(outer);
let _next = rt.scope(&mut pool);
assert!(
rt.get(5, &nobox()).is_none(),
"a new evaluation must not see it either"
);
}
fn names(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
fn set(v: &[&str]) -> FxHashSet<String> {
v.iter().map(|s| s.to_string()).collect()
}
fn analyse_pure(body: &Expr, idx: &[&str], consts: &[&str], arrays: &[&str]) -> ClassTable {
let mut t = ClassTable::default();
t.analyse(body, &names(idx), &[], &set(consts), &set(arrays));
t
}
fn is_pure(t: &ClassTable, e: &Expr) -> bool {
t.memoizable
.get(&(e as *const Expr as usize))
.is_some_and(|c| c & PURE_BIT != 0)
}
#[test]
fn maximal_box_pure_node_is_marked_and_its_children_are_not() {
let body = op(
"*",
vec![
Expr::Variable("u".into()), op(
"^",
vec![
op("sin", vec![Expr::Variable("j".into())]),
Expr::Integer(2),
],
),
],
);
let t = analyse_pure(&body, &["j"], &[], &[]);
let Expr::Operator(root) = &body else {
unreachable!()
};
let powi = &root.args[1];
let Expr::Operator(pn) = powi else {
unreachable!()
};
assert!(is_pure(&t, powi), "sin(j)^2 must be box-pure");
assert!(
!is_pure(&t, &pn.args[0]),
"the subsumed sin(j) must not carry its own entry"
);
assert!(!is_pure(&t, &body), "the root is never memoized");
}
#[test]
fn a_varying_leaf_blocks_box_purity() {
let body = op(
"*",
vec![
Expr::Variable("q".into()),
op(
"^",
vec![
op("sin", vec![Expr::Variable("theta".into())]),
Expr::Integer(2),
],
),
],
);
let t = analyse_pure(&body, &["j"], &[], &[]);
let Expr::Operator(root) = &body else {
unreachable!()
};
assert!(
!is_pure(&t, &root.args[1]),
"a subtree reading a non-CONST name must never be hoisted"
);
}
#[test]
fn t_is_never_a_box_binder() {
let body = op(
"*",
vec![
Expr::Variable("q".into()),
op("sin", vec![Expr::Variable("t".into())]),
],
);
let t = analyse_pure(&body, &["t"], &[], &[]);
let Expr::Operator(root) = &body else {
unreachable!()
};
assert!(!is_pure(&t, &root.args[1]), "sin(t) is time-varying");
}
#[test]
fn const_tier_leaves_keep_a_subtree_pure() {
let body = op(
"*",
vec![
Expr::Variable("q".into()),
op(
"*",
vec![
Expr::Variable("a_earth".into()),
op(
"index",
vec![Expr::Variable("coslat3".into()), Expr::Variable("j".into())],
),
],
),
],
);
let t = analyse_pure(&body, &["j"], &["a_earth", "coslat3"], &["coslat3"]);
let Expr::Operator(root) = &body else {
unreachable!()
};
assert!(is_pure(&t, &root.args[1]), "a_earth·coslat3[j] is CONST");
}
#[test]
fn scalar_only_const_subtree_is_not_cached() {
let body = op(
"*",
vec![
Expr::Variable("q".into()),
op(
"*",
vec![
Expr::Variable("a_earth".into()),
Expr::Variable("Rd".into()),
],
),
],
);
let t = analyse_pure(&body, &["j"], &["a_earth", "Rd"], &[]);
let Expr::Operator(root) = &body else {
unreachable!()
};
assert!(!is_pure(&t, &root.args[1]));
}
#[test]
fn nothing_below_a_nested_binder_is_hoisted() {
let inner = op(
"^",
vec![
op("sin", vec![Expr::Variable("j".into())]),
Expr::Integer(2),
],
);
let agg = Expr::operator(ExpressionNode {
op: "faq".to_string(),
args: vec![],
output_idx: Some(vec!["j".to_string()]),
expr: Some(Box::new(inner)),
..Default::default()
});
let body = op("*", vec![Expr::Variable("q".into()), agg]);
let t = analyse_pure(&body, &["j"], &[], &[]);
let Expr::Operator(root) = &body else {
unreachable!()
};
let Expr::Operator(agg_n) = &root.args[1] else {
unreachable!()
};
let inner_ref = agg_n.expr.as_deref().expect("body");
assert!(!is_pure(&t, inner_ref));
}
#[test]
fn box_signature_separates_distinguishable_boxes() {
let syms = names(&["i"]);
let other = names(&["k"]);
let cn = names(&["c"]);
let mk = |sy: &[String], lo: &[i64], sh: &[usize], cn: &[String], cv: &[i64]| {
box_sig(&VecBox {
syms: sy,
lo,
shape: sh,
cnames: cn,
cvals: cv,
})
};
let base = mk(&syms, &[1], &[4], &[], &[]);
assert_eq!(base, mk(&syms, &[1], &[4], &[], &[]), "same box, same key");
assert_ne!(base, mk(&syms, &[2], &[4], &[], &[]), "origin matters");
assert_ne!(base, mk(&syms, &[1], &[5], &[], &[]), "extent matters");
assert_ne!(base, mk(&other, &[1], &[4], &[], &[]), "symbols matter");
assert_ne!(
mk(&syms, &[1], &[4], &cn, &[0]),
mk(&syms, &[1], &[4], &cn, &[1]),
"the bound contraction tuple matters"
);
}
#[test]
fn pure_marks_reach_the_resolved_table() {
let sq = || {
op(
"^",
vec![
op("sin", vec![Expr::Variable("j".into())]),
Expr::Integer(2),
],
)
};
let body = op(
"*",
vec![Expr::Variable("u".into()), op("+", vec![sq(), sq()])],
);
let rt = CseRt::default();
rt.set_const_names(|| (FxHashSet::default(), FxHashSet::default()));
rt.analyse(&body, &names(&["j"]), &[]);
let Expr::Operator(root) = &body else {
unreachable!()
};
let Expr::Operator(plus) = &root.args[1] else {
unreachable!()
};
let mut pool = Pool::default();
let _s = rt.scope(&mut pool);
let c_plus = rt
.class_of(&root.args[1])
.expect("sin(j)^2+sin(j)^2 is classified");
assert!(
c_plus & PURE_BIT != 0,
"the maximal pure node's PURE mark must win in the resolved table"
);
for a in &plus.args {
let c = rt.class_of(a).expect("the repeated sq is plain-memoizable");
assert!(c & PURE_BIT == 0, "a subsumed child must stay plain");
}
let i = rt.inner.borrow();
for e in std::iter::once(&root.args[1]).chain(plus.args.iter()) {
let addr = e as *const Expr as usize;
assert_eq!(i.addrs.get(addr), i.classes.memoizable.get(&addr).copied());
}
}
fn shared(node: ExpressionNode) -> (Expr, std::sync::Arc<ExpressionNode>) {
let rc = std::sync::Arc::new(node);
(Expr::Operator(rc.clone()), rc)
}
#[test]
fn a_shared_cell_repeated_in_one_scope_is_memoizable() {
let (q, q_rc) = shared(ExpressionNode {
op: "sqrt".to_string(),
args: vec![op(
"-",
vec![Expr::Variable("a".into()), Expr::Variable("b".into())],
)],
..Default::default()
});
let body = op("+", vec![q.clone(), q]);
let rt = CseRt::default();
rt.analyse(&body, &[], &[]);
let mut pool = Pool::default();
let _s = rt.scope(&mut pool);
let inner = &q_rc.args[0];
assert!(
rt.class_of(inner).is_some(),
"the shared interior cell must be memoizable"
);
}
#[test]
fn a_marked_interior_cell_that_becomes_a_root_is_tombstoned() {
let (q, q_rc) = shared(ExpressionNode {
op: "sqrt".to_string(),
args: vec![op(
"-",
vec![Expr::Variable("a".into()), Expr::Variable("b".into())],
)],
..Default::default()
});
let body = op("+", vec![q.clone(), q.clone()]);
let rt = CseRt::default();
rt.analyse(&body, &[], &[]);
let mut pool = Pool::default();
let inner = &q_rc.args[0];
{
let _s = rt.scope(&mut pool);
assert!(rt.class_of(inner).is_some(), "interior mark exists first");
}
rt.analyse(inner, &[], &[]);
{
let _s = rt.scope(&mut pool);
assert!(
rt.class_of(inner).is_none(),
"a root address must never be served from the memo"
);
}
let body2 = op("*", vec![q.clone(), q]);
rt.analyse(&body2, &[], &[]);
{
let _s = rt.scope(&mut pool);
assert!(
rt.class_of(inner).is_none(),
"the tombstone must survive re-analysis by later bodies"
);
}
}
#[test]
fn a_pure_mark_is_refused_under_a_different_binder_context() {
let (p, p_rc) = shared(ExpressionNode {
op: "*".to_string(),
args: vec![
Expr::Variable("s".into()),
op(
"^",
vec![
op("sin", vec![Expr::Variable("j".into())]),
Expr::Integer(2),
],
),
],
..Default::default()
});
let body_a = op("+", vec![Expr::Variable("u".into()), p.clone()]);
let body_b = op("+", vec![Expr::Variable("q".into()), p]);
let rt = CseRt::default();
rt.set_const_names(|| (FxHashSet::default(), FxHashSet::default()));
rt.analyse(&body_a, &names(&["j"]), &[]);
rt.analyse(&body_b, &names(&["k"]), &[]);
let hot = &p_rc.args[1];
let mut pool = Pool::default();
let _s = rt.scope(&mut pool);
let class = rt.class_of(hot).expect("the ^ cell is classified");
assert!(class & PURE_BIT != 0, "PURE mark minted under rule A");
let jsyms = names(&["j"]);
let ksyms = names(&["k"]);
let box_j = VecBox {
syms: &jsyms,
lo: &[0],
shape: &[4],
cnames: &[],
cvals: &[],
};
let box_k = VecBox {
syms: &ksyms,
lo: &[0],
shape: &[4],
cnames: &[],
cvals: &[],
};
rt.put(class, VecValue::Scalar(1.5), &box_j);
assert!(
matches!(rt.get(class, &box_j), Some(VecValue::Scalar(v)) if v == 1.5),
"the mark works under the binder set it was proven under"
);
assert!(
rt.get(class, &box_k).is_none(),
"a PURE mark must not be served under a different binder set"
);
let out = rt.put(class, VecValue::Scalar(9.0), &box_k);
assert!(
matches!(out, VecValue::Scalar(v) if v == 9.0),
"passthrough"
);
assert!(
rt.get(class, &box_k).is_none(),
"a mismatched-context store must be refused"
);
assert!(
matches!(rt.get(class, &box_j), Some(VecValue::Scalar(v)) if v == 1.5),
"the proven-context entry is untouched"
);
}
}