use crate::kernel::{
domain::Domain,
expr::{BigFloat, BigInt, BigRat, ExprData, ExprId},
};
use std::fmt;
pub const POS_INFINITY_SYMBOL: &str = "\u{221e}";
use hashbrown::HashTable;
use std::hash::BuildHasher;
use std::sync::{PoisonError, RwLock};
#[derive(Clone, Copy)]
struct Slot {
id: u32,
hash: u32,
}
#[inline]
fn slot_hash(h32: u32) -> u64 {
let h = u64::from(h32);
h | (h << 32)
}
#[repr(align(128))]
struct Shard(RwLock<HashTable<Slot>>);
fn shard_count() -> usize {
#[cfg(feature = "parallel")]
{
static SHARDS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
*SHARDS.get_or_init(|| {
let cores = std::thread::available_parallelism().map_or(1, |n| n.get());
(cores * 4).next_power_of_two().clamp(4, 64)
})
}
#[cfg(not(feature = "parallel"))]
{
1
}
}
struct PoolIndex {
shards: Box<[Shard]>,
shard_shift: u32,
seed: foldhash::fast::RandomState,
}
impl PoolIndex {
fn new() -> Self {
let n = shard_count();
debug_assert!(n.is_power_of_two());
PoolIndex {
shards: (0..n)
.map(|_| Shard(RwLock::new(HashTable::new())))
.collect(),
shard_shift: 64 - n.trailing_zeros(),
seed: foldhash::fast::RandomState::default(),
}
}
fn get_or_insert_with(
&self,
nodes: &boxcar::Vec<Node>,
data: ExprData,
make: impl FnOnce(ExprData) -> ExprId,
) -> ExprId {
let hash = self.seed.hash_one(&data);
self.get_or_insert_hashed(nodes, hash, data, make)
}
fn get_or_insert_hashed(
&self,
nodes: &boxcar::Vec<Node>,
hash: u64,
data: ExprData,
make: impl FnOnce(ExprData) -> ExprId,
) -> ExprId {
let h32 = hash as u32;
let th = slot_hash(h32);
let shard = hash.checked_shr(self.shard_shift).unwrap_or(0) as usize;
let lock = &self.shards[shard].0;
let eq = |s: &Slot| s.hash == h32 && nodes[s.id as usize].data == data;
if let Some(s) = lock
.read()
.unwrap_or_else(PoisonError::into_inner)
.find(th, eq)
{
return ExprId(s.id);
}
let mut table = lock.write().unwrap_or_else(PoisonError::into_inner);
if let Some(s) = table.find(th, eq) {
return ExprId(s.id);
}
let id = make(data);
table.insert_unique(
th,
Slot {
id: id.0,
hash: h32,
},
|s| slot_hash(s.hash),
);
id
}
}
struct Node {
data: ExprData,
mult_commutative: bool,
depth: u32,
static_domain_fact: bool,
non_finite: bool,
}
pub struct ExprPool {
nodes: boxcar::Vec<Node>,
index: PoolIndex,
}
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ExprPool>();
assert_send_sync::<Node>();
assert_send_sync::<ExprData>();
};
pub(crate) const MAX_FLAT_ARITY: usize = 131_072;
impl ExprPool {
pub fn new() -> Self {
ExprPool {
nodes: boxcar::Vec::new(),
index: PoolIndex::new(),
}
}
pub fn intern(&self, data: ExprData) -> ExprId {
let data = match self.canonical_form(data) {
Ok(d) => d,
Err(id) => return id,
};
let make = |d: ExprData| {
let node = self.make_node(d);
ExprId(self.nodes.push(node) as u32)
};
self.index.get_or_insert_with(&self.nodes, data, make)
}
fn canonical_form(&self, data: ExprData) -> Result<ExprData, ExprId> {
match data {
ExprData::Rational(r) if *r.0.denom() == 1 => {
Ok(ExprData::Integer(BigInt(r.0.into_numer_denom().0)))
}
ExprData::Float(mut f)
if (f.inner.is_zero() || f.inner.is_nan()) && f.inner.is_sign_negative() =>
{
f.inner.abs_mut();
Ok(ExprData::Float(f))
}
ExprData::Add(args) if args.len() <= 1 => match args.first() {
Some(&only) => Err(only),
None => Err(self.integer(0_i32)),
},
ExprData::Mul(args) if args.len() <= 1 => match args.first() {
Some(&only) => Err(only),
None => Err(self.integer(1_i32)),
},
other => Ok(other),
}
}
fn make_node(&self, data: ExprData) -> Node {
let mult_commutative = self.compute_mult_commutative(&data);
let depth = self.compute_depth(&data);
let static_domain_fact = self.compute_static_domain_fact(&data);
let non_finite = self.compute_non_finite(&data);
Node {
data,
mult_commutative,
depth,
static_domain_fact,
non_finite,
}
}
fn compute_depth(&self, data: &ExprData) -> u32 {
let child = |c: ExprId| self.depth(c);
let deepest = match data {
ExprData::Symbol { .. }
| ExprData::Integer(_)
| ExprData::Rational(_)
| ExprData::Float(_) => 0,
ExprData::Add(args) | ExprData::Mul(args) => {
args.iter().copied().map(child).max().unwrap_or(0)
}
ExprData::Pow { base, exp } => child(*base).max(child(*exp)),
ExprData::Func { args, .. } => args.iter().copied().map(child).max().unwrap_or(0),
ExprData::Piecewise { branches, default } => branches
.iter()
.map(|&(c, v)| child(c).max(child(v)))
.max()
.unwrap_or(0)
.max(child(*default)),
ExprData::Predicate { args, .. } => args.iter().copied().map(child).max().unwrap_or(0),
ExprData::Forall { var, body } | ExprData::Exists { var, body } => {
child(*var).max(child(*body))
}
ExprData::BigO(inner) => child(*inner),
ExprData::RootSum { poly, body, .. } => child(*poly).max(child(*body)),
};
deepest.saturating_add(1)
}
fn compute_mult_commutative(&self, data: &ExprData) -> bool {
let child = |c: ExprId| self.is_mult_commutative(c);
match data {
ExprData::Symbol { commutative, .. } => *commutative,
ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => true,
ExprData::Add(args) | ExprData::Mul(args) => args.iter().copied().all(child),
ExprData::Pow { base, exp } => child(*base) && child(*exp),
ExprData::Func { args, .. } => args.iter().copied().all(child),
ExprData::Piecewise { branches, default } => {
branches.iter().all(|&(c, v)| child(c) && child(v)) && child(*default)
}
ExprData::Predicate { args, .. } => args.iter().copied().all(child),
ExprData::Forall { var, body } | ExprData::Exists { var, body } => {
child(*var) && child(*body)
}
ExprData::BigO(inner) => child(*inner),
ExprData::RootSum { poly, body, .. } => child(*poly) && child(*body),
}
}
pub fn is_mult_commutative(&self, id: ExprId) -> bool {
self.node(id).mult_commutative
}
fn compute_static_domain_fact(&self, data: &ExprData) -> bool {
let child = |c: ExprId| self.node(c).static_domain_fact;
match data {
ExprData::Symbol { domain, .. } => {
matches!(domain, Domain::Positive | Domain::NonZero)
}
ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => false,
ExprData::Add(args)
| ExprData::Mul(args)
| ExprData::Func { args, .. }
| ExprData::Predicate { args, .. } => args.iter().copied().any(child),
ExprData::Pow { base, exp } => child(*base) || child(*exp),
ExprData::Piecewise { branches, default } => {
branches.iter().any(|&(c, v)| child(c) || child(v)) || child(*default)
}
ExprData::Forall { var, body } | ExprData::Exists { var, body } => {
child(*var) || child(*body)
}
ExprData::BigO(inner) => child(*inner),
ExprData::RootSum { poly, var, body } => child(*poly) || child(*var) || child(*body),
}
}
fn compute_non_finite(&self, data: &ExprData) -> bool {
let child = |c: ExprId| self.node(c).non_finite;
match data {
ExprData::Symbol { name, .. } => name == POS_INFINITY_SYMBOL,
ExprData::Float(f) => !f.inner.is_finite(),
ExprData::Integer(_) | ExprData::Rational(_) => false,
ExprData::Add(args)
| ExprData::Mul(args)
| ExprData::Func { args, .. }
| ExprData::Predicate { args, .. } => args.iter().copied().any(child),
ExprData::Pow { base, exp } => child(*base) || child(*exp),
ExprData::Piecewise { branches, default } => {
branches.iter().any(|&(c, v)| child(c) || child(v)) || child(*default)
}
ExprData::Forall { var, body } | ExprData::Exists { var, body } => {
child(*var) || child(*body)
}
ExprData::BigO(inner) => child(*inner),
ExprData::RootSum { poly, var, body } => child(*poly) || child(*var) || child(*body),
}
}
pub(crate) fn has_non_finite(&self, id: ExprId) -> bool {
self.node(id).non_finite
}
pub(crate) fn has_static_domain_fact(&self, id: ExprId) -> bool {
self.node(id).static_domain_fact
}
pub fn depth(&self, id: ExprId) -> u32 {
self.node(id).depth
}
fn node(&self, id: ExprId) -> &Node {
self.nodes
.get(id.0 as usize)
.expect("ExprPool: ExprId out of range")
}
pub fn with<R, F: FnOnce(&ExprData) -> R>(&self, id: ExprId, f: F) -> R {
f(&self.node(id).data)
}
pub fn get(&self, id: ExprId) -> ExprData {
self.with(id, |d| d.clone())
}
pub fn len(&self) -> usize {
self.nodes.count()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn symbol(&self, name: impl Into<String>, domain: Domain) -> ExprId {
self.symbol_commutative(name, domain, true)
}
pub const IMAGINARY_UNIT_NAME: &'static str = "I";
pub fn imaginary_unit(&self) -> ExprId {
self.symbol(Self::IMAGINARY_UNIT_NAME, Domain::Complex)
}
pub fn is_imaginary_unit(&self, id: ExprId) -> bool {
self.with(id, |d| {
matches!(
d,
ExprData::Symbol { name, domain, .. }
if name == Self::IMAGINARY_UNIT_NAME && *domain == Domain::Complex
)
})
}
pub fn symbol_commutative(
&self,
name: impl Into<String>,
domain: Domain,
commutative: bool,
) -> ExprId {
self.intern(ExprData::Symbol {
name: name.into(),
domain,
commutative,
})
}
pub fn integer(&self, n: impl Into<rug::Integer>) -> ExprId {
self.intern(ExprData::Integer(BigInt(n.into())))
}
pub fn rational(
&self,
numer: impl Into<rug::Integer>,
denom: impl Into<rug::Integer>,
) -> ExprId {
let r = rug::Rational::from((numer.into(), denom.into()));
self.intern(ExprData::Rational(BigRat(r)))
}
pub fn float(&self, value: f64, prec: u32) -> ExprId {
let f = rug::Float::with_val(prec, value);
self.intern(ExprData::Float(BigFloat { inner: f, prec }))
}
fn flatten_assoc(&self, args: Vec<ExprId>, want_mul: bool) -> Vec<ExprId> {
fn splices(data: &ExprData, want_mul: bool) -> Option<&Vec<ExprId>> {
match data {
ExprData::Mul(children) if want_mul => Some(children),
ExprData::Add(children) if !want_mul => Some(children),
_ => None,
}
}
if !args
.iter()
.any(|&a| splices(&self.node(a).data, want_mul).is_some())
{
return args;
}
let mut out = Vec::with_capacity(args.len() + 4);
let mut stack: Vec<ExprId> = args.iter().rev().copied().collect();
while let Some(id) = stack.pop() {
match splices(&self.node(id).data, want_mul) {
Some(children) => {
if out.len() + stack.len() + children.len() > MAX_FLAT_ARITY {
return args;
}
stack.extend(children.iter().rev().copied());
}
None => out.push(id),
}
}
out
}
pub fn add(&self, args: Vec<ExprId>) -> ExprId {
let mut args = self.flatten_assoc(args, false);
args.sort_unstable();
self.intern(ExprData::Add(args))
}
pub fn mul(&self, args: Vec<ExprId>) -> ExprId {
let mut args = self.flatten_assoc(args, true);
let sort_ok = args
.iter()
.all(|&a| crate::kernel::expr_props::mult_tree_is_commutative(self, a));
if sort_ok {
args.sort_unstable();
}
self.intern(ExprData::Mul(args))
}
pub fn pow(&self, base: ExprId, exp: ExprId) -> ExprId {
self.intern(ExprData::Pow { base, exp })
}
pub fn func(&self, name: impl Into<String>, args: Vec<ExprId>) -> ExprId {
self.intern(ExprData::Func {
name: name.into(),
args,
})
}
pub fn try_func(
&self,
name: impl Into<String>,
args: Vec<ExprId>,
) -> Result<ExprId, crate::kernel::FuncArityError> {
let name = name.into();
crate::kernel::FuncArityError::check(&name, args.len())?;
Ok(self.func(name, args))
}
pub fn piecewise(&self, branches: Vec<(ExprId, ExprId)>, default: ExprId) -> ExprId {
self.intern(ExprData::Piecewise { branches, default })
}
pub fn predicate(&self, kind: crate::kernel::expr::PredicateKind, args: Vec<ExprId>) -> ExprId {
self.intern(ExprData::Predicate { kind, args })
}
pub fn pred_lt(&self, a: ExprId, b: ExprId) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::Lt, vec![a, b])
}
pub fn pred_le(&self, a: ExprId, b: ExprId) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::Le, vec![a, b])
}
pub fn pred_gt(&self, a: ExprId, b: ExprId) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::Gt, vec![a, b])
}
pub fn pred_ge(&self, a: ExprId, b: ExprId) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::Ge, vec![a, b])
}
pub fn pred_eq(&self, a: ExprId, b: ExprId) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::Eq, vec![a, b])
}
pub fn pred_ne(&self, a: ExprId, b: ExprId) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::Ne, vec![a, b])
}
pub fn pred_and(&self, args: Vec<ExprId>) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::And, args)
}
pub fn pred_or(&self, args: Vec<ExprId>) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::Or, args)
}
pub fn pred_not(&self, a: ExprId) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::Not, vec![a])
}
pub fn pred_true(&self) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::True, vec![])
}
pub fn pred_false(&self) -> ExprId {
self.predicate(crate::kernel::expr::PredicateKind::False, vec![])
}
pub fn forall(&self, var: ExprId, body: ExprId) -> ExprId {
self.intern(ExprData::Forall { var, body })
}
pub fn exists(&self, var: ExprId, body: ExprId) -> ExprId {
self.intern(ExprData::Exists { var, body })
}
pub fn root_sum(&self, poly: ExprId, var: ExprId, body: ExprId) -> ExprId {
self.intern(ExprData::RootSum { poly, var, body })
}
pub fn big_o(&self, arg: ExprId) -> ExprId {
self.intern(ExprData::BigO(arg))
}
pub fn pos_infinity(&self) -> ExprId {
self.symbol(POS_INFINITY_SYMBOL, Domain::Positive)
}
pub fn display(&self, id: ExprId) -> ExprDisplay<'_> {
ExprDisplay { id, pool: self }
}
}
impl Default for ExprPool {
fn default() -> Self {
Self::new()
}
}
pub struct ExprDisplay<'a> {
pub id: ExprId,
pub pool: &'a ExprPool,
}
impl fmt::Display for ExprDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let data = self.pool.get(self.id);
fmt_data(&data, self.pool, f)
}
}
impl fmt::Debug for ExprDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self)
}
}
fn fmt_pow_atom(id: ExprId, pool: &ExprPool) -> String {
let s = pool.display(id).to_string();
let needs_parens = match pool.get(id) {
ExprData::Symbol { .. } | ExprData::Integer(_) | ExprData::Float(_) => false,
ExprData::Func { .. } => false,
ExprData::Add(_) | ExprData::Mul(_) => false,
ExprData::Rational(_)
| ExprData::Pow { .. }
| ExprData::Piecewise { .. }
| ExprData::Predicate { .. }
| ExprData::Forall { .. }
| ExprData::Exists { .. }
| ExprData::BigO(_)
| ExprData::RootSum { .. } => true,
};
if needs_parens {
format!("({s})")
} else {
s
}
}
fn fmt_pow_base(id: ExprId, pool: &ExprPool) -> String {
let s = fmt_pow_atom(id, pool);
if s.starts_with('-') {
format!("({s})")
} else {
s
}
}
fn fmt_data(data: &ExprData, pool: &ExprPool, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match data {
ExprData::Symbol { name, .. } => write!(f, "{}", name),
ExprData::Integer(n) => write!(f, "{}", n),
ExprData::Rational(r) => write!(f, "{}", r),
ExprData::Float(fl) => write!(f, "{}", fl),
ExprData::Add(args) => {
write!(f, "(")?;
for (i, &arg) in args.iter().enumerate() {
if i > 0 {
write!(f, " + ")?;
}
write!(f, "{}", pool.display(arg))?;
}
write!(f, ")")
}
ExprData::Mul(args) => {
write!(f, "(")?;
for (i, &arg) in args.iter().enumerate() {
if i > 0 {
write!(f, " * ")?;
}
write!(f, "{}", pool.display(arg))?;
}
write!(f, ")")
}
ExprData::Pow { base, exp } => {
let base_s = fmt_pow_base(*base, pool);
let exp_s = fmt_pow_atom(*exp, pool);
write!(f, "{base_s}^{exp_s}")
}
ExprData::Func { name, args } => {
write!(f, "{}(", name)?;
for (i, &arg) in args.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", pool.display(arg))?;
}
write!(f, ")")
}
ExprData::Piecewise { branches, default } => {
write!(f, "Piecewise(")?;
for (i, (cond, val)) in branches.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "({}, {})", pool.display(*cond), pool.display(*val))?;
}
write!(f, "; default={})", pool.display(*default))
}
ExprData::Predicate { kind, args } => match kind {
crate::kernel::expr::PredicateKind::True => write!(f, "True"),
crate::kernel::expr::PredicateKind::False => write!(f, "False"),
crate::kernel::expr::PredicateKind::Not if args.len() == 1 => {
write!(f, "¬({})", pool.display(args[0]))
}
crate::kernel::expr::PredicateKind::And | crate::kernel::expr::PredicateKind::Or => {
write!(f, "(")?;
for (i, &arg) in args.iter().enumerate() {
if i > 0 {
write!(f, " {} ", kind)?;
}
write!(f, "{}", pool.display(arg))?;
}
write!(f, ")")
}
_ if args.len() == 2 => {
write!(
f,
"({} {} {})",
pool.display(args[0]),
kind,
pool.display(args[1])
)
}
_ => {
write!(f, "{kind}(")?;
for (i, &arg) in args.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", pool.display(arg))?;
}
write!(f, ")")
}
},
ExprData::Forall { var, body } => {
write!(f, "∀ {} . {}", pool.display(*var), pool.display(*body))
}
ExprData::Exists { var, body } => {
write!(f, "∃ {} . {}", pool.display(*var), pool.display(*body))
}
ExprData::BigO(arg) => {
write!(f, "O({})", pool.display(*arg))
}
ExprData::RootSum { poly, var, body } => {
write!(
f,
"RootSum({}, {} . {})",
pool.display(*poly),
pool.display(*var),
pool.display(*body)
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::domain::Domain;
fn pool() -> ExprPool {
ExprPool::new()
}
#[test]
fn noncommutative_mul_orders_distinct() {
let p = pool();
let a = p.symbol_commutative("A", Domain::Real, false);
let b = p.symbol_commutative("B", Domain::Real, false);
assert_ne!(
p.mul(vec![a, b]),
p.mul(vec![b, a]),
"A*B and B*A must not hash-cons together for NC symbols"
);
}
#[test]
fn symbol_commutative_is_structural() {
let p = pool();
let xc = p.symbol_commutative("x", Domain::Real, true);
let xnc = p.symbol_commutative("x", Domain::Real, false);
assert_ne!(xc, xnc);
}
#[test]
fn symbol_interning() {
let p = pool();
let x1 = p.symbol("x", Domain::Real);
let x2 = p.symbol("x", Domain::Real);
assert_eq!(x1, x2, "same symbol must return same ExprId");
}
#[test]
fn domain_is_structural() {
let p = pool();
let xr = p.symbol("x", Domain::Real);
let xc = p.symbol("x", Domain::Complex);
assert_ne!(xr, xc, "same name but different domain must be distinct");
}
#[test]
fn integer_interning() {
let p = pool();
let a = p.integer(42_i32);
let b = p.integer(42_i32);
let c = p.integer(99_i32);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn rational_canonical() {
let p = pool();
let r1 = p.rational(2_i32, 4_i32);
let r2 = p.rational(1_i32, 2_i32);
assert_eq!(r1, r2, "rationals must be reduced to canonical form");
}
#[test]
fn rational_with_unit_denominator_is_the_integer() {
let p = pool();
assert_eq!(p.rational(4_i32, 2_i32), p.integer(2_i32));
assert_eq!(p.rational(-6_i32, 3_i32), p.integer(-2_i32));
assert_eq!(p.rational(0_i32, 5_i32), p.integer(0_i32));
assert!(matches!(
p.get(p.rational(4_i32, 2_i32)),
ExprData::Integer(_)
));
let raw = p.intern(ExprData::Rational(BigRat(rug::Rational::from(7))));
assert_eq!(raw, p.integer(7_i32));
}
#[test]
fn unary_and_empty_sums_and_products_collapse() {
let p = pool();
let x = p.symbol("x", Domain::Real);
assert_eq!(p.add(vec![x]), x);
assert_eq!(p.mul(vec![x]), x);
assert_eq!(p.add(vec![]), p.integer(0_i32));
assert_eq!(p.mul(vec![]), p.integer(1_i32));
assert_eq!(p.add(vec![p.add(vec![x])]), x);
assert_eq!(p.intern(ExprData::Mul(vec![x])), x);
assert_eq!(p.intern(ExprData::Add(vec![])), p.integer(0_i32));
let a = p.symbol_commutative("A", Domain::Real, false);
assert_eq!(p.mul(vec![a]), a);
assert_eq!(p.display(p.add(vec![])).to_string(), "0");
assert_eq!(p.display(p.mul(vec![])).to_string(), "1");
}
#[test]
fn signed_zero_floats_are_one_node_stored_positive() {
let p = pool();
let neg = p.float(-0.0, 53);
let pos = p.float(0.0, 53);
assert_eq!(neg, pos);
match p.get(neg) {
ExprData::Float(f) => assert!(f.inner.is_zero() && !f.inner.is_sign_negative()),
other => panic!("{other:?}"),
}
assert_eq!(p.display(neg).to_string(), "0.0");
let n1 = p.float(f64::NAN, 53);
let n2 = p.float(-f64::NAN, 53);
assert_eq!(n1, n2);
}
#[test]
fn float_display_is_never_integer_shaped() {
let p = pool();
for (v, prec) in [
(0.0, 53),
(1.0, 53),
(2.0_f64.powi(60), 53),
(3.0, 64),
(0.0, 200),
] {
let s = p.display(p.float(v, prec)).to_string();
assert!(s.contains(['.', 'e']), "{v} at {prec} printed as {s}");
match p.get(crate::parse::parse(&s, &p, &mut Default::default()).unwrap()) {
ExprData::Float(_) => {}
other => panic!("{s} re-parsed as {other:?}"),
}
}
}
#[test]
fn float_precision_is_structural() {
let p = pool();
let f53 = p.float(1.0, 53);
let f64_ = p.float(1.0, 64);
assert_ne!(
f53, f64_,
"same value but different precision is a different expr"
);
}
#[test]
fn subexpression_sharing() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let two = p.integer(2_i32);
let xsq1 = p.pow(x, two);
let xsq2 = p.pow(x, two);
assert_eq!(xsq1, xsq2);
assert_eq!(p.len(), 3);
}
#[test]
fn add_interning() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
let s1 = p.add(vec![x, y]);
let s2 = p.add(vec![x, y]);
assert_eq!(s1, s2);
}
#[test]
fn arg_order_is_canonical() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
let s1 = p.add(vec![x, y]);
let s2 = p.add(vec![y, x]);
assert_eq!(s1, s2, "a+b and b+a must be the same expression after PA-3");
let m1 = p.mul(vec![x, y]);
let m2 = p.mul(vec![y, x]);
assert_eq!(m1, m2, "a*b and b*a must be the same expression after PA-3");
}
#[test]
fn func_interning() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let s1 = p.func("sin", vec![x]);
let s2 = p.func("sin", vec![x]);
let c1 = p.func("cos", vec![x]);
assert_eq!(s1, s2);
assert_ne!(s1, c1);
}
fn nary_args(p: &ExprPool, id: ExprId) -> Vec<ExprId> {
p.with(id, |d| match d {
ExprData::Add(a) | ExprData::Mul(a) => a.clone(),
other => panic!("expected an Add or Mul, got {other:?}"),
})
}
#[test]
fn mul_splices_nested_children() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
let z = p.symbol("z", Domain::Real);
let flat = p.mul(vec![x, y, z]);
let left = p.mul(vec![p.mul(vec![x, y]), z]); let right = p.mul(vec![x, p.mul(vec![y, z])]);
assert_eq!(left, flat, "(x*y)*z must intern as the flat x*y*z");
assert_eq!(right, flat, "x*(y*z) must intern as the flat x*y*z");
assert_eq!(nary_args(&p, flat).len(), 3);
}
#[test]
fn add_splices_nested_children() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
let z = p.symbol("z", Domain::Real);
let flat = p.add(vec![x, y, z]);
assert_eq!(p.add(vec![p.add(vec![x, y]), z]), flat);
assert_eq!(p.add(vec![x, p.add(vec![y, z])]), flat);
assert_eq!(nary_args(&p, flat).len(), 3);
}
#[test]
fn splicing_does_not_cross_operators() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
let z = p.symbol("z", Domain::Real);
let sum = p.add(vec![x, y]);
let prod = p.mul(vec![sum, z]); assert_eq!(nary_args(&p, prod).len(), 2);
assert_ne!(prod, p.mul(vec![x, y, z]));
let prod2 = p.mul(vec![x, y]);
let sum2 = p.add(vec![prod2, z]); assert_eq!(nary_args(&p, sum2).len(), 2);
assert_ne!(sum2, p.add(vec![x, y, z]));
}
#[test]
fn splicing_preserves_order_for_noncommutative_generators() {
let p = pool();
let a = p.symbol_commutative("A", Domain::Real, false);
let b = p.symbol_commutative("B", Domain::Real, false);
let c = p.symbol_commutative("C", Domain::Real, false);
let flat = p.mul(vec![a, b, c]);
assert_eq!(p.mul(vec![p.mul(vec![a, b]), c]), flat);
assert_eq!(p.mul(vec![a, p.mul(vec![b, c])]), flat);
assert_eq!(p.display(flat).to_string(), "(A * B * C)");
assert_ne!(flat, p.mul(vec![b, a, c]));
}
#[test]
fn splicing_a_deeply_nested_chain_does_not_recurse() {
const N: i64 = 50_000;
let p = pool();
let x = p.symbol("x", Domain::Real);
let mut acc = x;
for i in 0..N {
let k = p.integer(i);
acc = p.intern(ExprData::Add(vec![acc, k]));
}
assert_eq!(p.depth(acc), N as u32 + 1);
let flat = p.add(vec![acc]);
assert_eq!(p.depth(flat), 2);
assert_eq!(nary_args(&p, flat).len(), N as usize + 1);
}
#[test]
fn empty_same_operator_children_are_the_identity_literals() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let empty_mul = p.mul(vec![]);
assert_eq!(empty_mul, p.integer(1_i32));
assert_eq!(p.mul(vec![x, empty_mul]), p.mul(vec![x, p.integer(1_i32)]));
let empty_add = p.add(vec![]);
assert_eq!(empty_add, p.integer(0_i32));
assert_eq!(p.add(vec![x, empty_add]), p.add(vec![x, p.integer(0_i32)]));
}
#[test]
fn flattening_improves_hash_consing() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
let z = p.symbol("z", Domain::Real);
let before = p.len();
p.mul(vec![p.mul(vec![x, y]), z]);
p.mul(vec![x, p.mul(vec![y, z])]);
p.mul(vec![x, y, z]);
assert_eq!(p.len() - before, 3);
}
#[test]
fn display_symbol() {
let p = pool();
let x = p.symbol("x", Domain::Real);
assert_eq!(p.display(x).to_string(), "x");
}
#[test]
fn display_integer() {
let p = pool();
let n = p.integer(42_i32);
assert_eq!(p.display(n).to_string(), "42");
}
#[test]
fn display_pow() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let two = p.integer(2_i32);
let xsq = p.pow(x, two);
assert_eq!(p.display(xsq).to_string(), "x^2");
}
#[test]
fn display_add() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
let s = p.add(vec![x, y]);
assert_eq!(p.display(s).to_string(), "(x + y)");
}
#[test]
fn display_func() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let s = p.func("sin", vec![x]);
assert_eq!(p.display(s).to_string(), "sin(x)");
}
#[test]
fn display_nested() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let two = p.integer(2_i32);
let xsq = p.pow(x, two);
let one = p.integer(1_i32);
let expr = p.add(vec![xsq, one]);
assert_eq!(p.display(expr).to_string(), "(x^2 + 1)");
}
#[test]
fn display_negative_pow_base_is_parenthesised() {
let p = pool();
let n = p.symbol("n", Domain::Real);
let m1 = p.integer(-1_i32);
assert_eq!(p.display(p.pow(m1, n)).to_string(), "(-1)^n");
let m2 = p.integer(-2_i32);
assert_eq!(p.display(p.pow(m2, n)).to_string(), "(-2)^n");
let half = p.rational(-1, 2);
assert_eq!(p.display(p.pow(half, n)).to_string(), "(-1/2)^n");
let m3 = p.integer(-3_i32);
assert_eq!(p.display(p.pow(m2, m3)).to_string(), "(-2)^-3");
let m16 = p.integer(-16_i32);
let prod = p.mul(vec![m16, p.pow(m2, n)]);
assert_eq!(p.display(prod).to_string(), "((-2)^n * -16)");
}
#[test]
fn display_positive_pow_base_is_bare() {
let p = pool();
let n = p.symbol("n", Domain::Real);
let two = p.integer(2_i32);
assert_eq!(p.display(p.pow(two, n)).to_string(), "2^n");
let x = p.symbol("x", Domain::Real);
assert_eq!(p.display(p.pow(x, n)).to_string(), "x^n");
}
fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn pool_is_send_sync() {
assert_send_sync::<ExprPool>();
}
}
#[cfg(test)]
mod flat_arity_cap_tests {
use super::*;
use crate::kernel::Domain;
#[test]
fn self_combination_terminates_instead_of_doubling_forever() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let mut e = pool.mul(vec![x, pool.integer(2_i32)]);
for _ in 0..22 {
e = pool.mul(vec![e, e]);
}
let width = match &pool.get(e) {
ExprData::Mul(children) => children.len(),
_ => 1,
};
assert!(
width <= MAX_FLAT_ARITY,
"a declined splice must leave a bounded node, got {width} children"
);
}
#[test]
fn a_large_honest_sum_still_flattens() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let _ = x;
let terms: Vec<_> = (0..60_000).map(|i| pool.integer(i)).collect();
let sum = pool.add(terms);
let ExprData::Add(children) = &pool.get(sum) else {
panic!("expected a flat Add");
};
assert!(
children.len() >= 59_000,
"a 60k-term sum must stay flat, got {} children",
children.len()
);
}
#[test]
fn ordinary_nesting_still_splices() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
let z = pool.symbol("z", Domain::Real);
let inner = pool.mul(vec![x, y]);
assert_eq!(pool.mul(vec![inner, z]), pool.mul(vec![x, y, z]));
}
}
#[cfg(test)]
mod intern_index_tests {
use super::*;
use crate::kernel::Domain;
use std::sync::Barrier;
#[test]
fn concurrent_intern_of_the_same_values_agrees() {
const THREADS: usize = 8;
const N: i64 = 2_000;
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let before = pool.len();
let barrier = Barrier::new(THREADS);
let per_thread: Vec<Vec<ExprId>> = std::thread::scope(|s| {
let handles: Vec<_> = (0..THREADS)
.map(|t| {
let (pool, barrier) = (&pool, &barrier);
s.spawn(move || {
barrier.wait();
let stride = [1, 3, 7, 9, 11, 13, 17, 19][t];
let mut ids = vec![ExprId(u32::MAX); 4 * N as usize];
for j in 0..N {
let k = (j * stride) % N;
let i = k as usize;
ids[4 * i] = pool.integer(k - N / 2);
ids[4 * i + 1] = pool.rational(k, 7);
ids[4 * i + 2] = pool.float(k as f64 * 0.25, 53);
ids[4 * i + 3] = pool.add(vec![x, pool.integer(k + N)]);
}
ids
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
for ids in &per_thread[1..] {
assert_eq!(ids, &per_thread[0], "threads disagree on an id");
}
let serial = ExprPool::new();
let sx = serial.symbol("x", Domain::Real);
let serial_before = serial.len();
for k in 0..N {
serial.integer(k - N / 2);
serial.rational(k, 7);
serial.float(k as f64 * 0.25, 53);
serial.add(vec![sx, serial.integer(k + N)]);
}
assert_eq!(
pool.len() - before,
serial.len() - serial_before,
"a racing intern created a duplicate node"
);
for k in 0..N {
let i = 4 * k as usize;
assert_eq!(
pool.get(per_thread[0][i]),
ExprData::Integer(crate::kernel::expr::BigInt(rug::Integer::from(k - N / 2)))
);
}
}
fn intern_with_hash(pool: &ExprPool, hash: u64, data: ExprData) -> ExprId {
pool.index
.get_or_insert_hashed(&pool.nodes, hash, data, |d| {
ExprId(pool.nodes.push(pool.make_node(d)) as u32)
})
}
fn int(n: i64) -> ExprData {
ExprData::Integer(crate::kernel::expr::BigInt(rug::Integer::from(n)))
}
#[test]
fn forced_hash_collision_keeps_nodes_distinct() {
let pool = ExprPool::new();
let ids: Vec<ExprId> = (0..200)
.map(|k| intern_with_hash(&pool, 7, int(k)))
.collect();
let distinct: std::collections::HashSet<ExprId> = ids.iter().copied().collect();
assert_eq!(distinct.len(), 200, "a hash collision merged two nodes");
for (k, &id) in ids.iter().enumerate() {
assert_eq!(pool.get(id), int(k as i64));
assert_eq!(intern_with_hash(&pool, 7, int(k as i64)), id);
}
let a = intern_with_hash(&pool, 0x0000_0000_dead_beef, int(-1));
let b = intern_with_hash(&pool, 0xffff_ffff_dead_beef, int(-2));
assert_ne!(a, b);
assert_eq!(pool.len(), 202);
}
fn assert_hash_consed(pool: &ExprPool) {
let mut seen = std::collections::HashMap::new();
for i in 0..pool.len() {
let id = ExprId(i as u32);
let data = pool.get(id);
assert_eq!(
pool.intern(data.clone()),
id,
"node {i} re-interned elsewhere"
);
if let Some(prev) = seen.insert(data, id) {
panic!("nodes {prev:?} and {id:?} hold equal data");
}
}
}
#[test]
fn concurrent_compound_interning_stays_hash_consed() {
const THREADS: usize = 8;
const N: i64 = 1_500;
let pool = ExprPool::new();
let barrier = Barrier::new(THREADS);
let per_thread: Vec<Vec<ExprId>> = std::thread::scope(|s| {
let handles: Vec<_> = (0..THREADS)
.map(|t| {
let (pool, barrier) = (&pool, &barrier);
s.spawn(move || {
let x = pool.symbol("x", Domain::Real);
let y = pool.symbol("y", Domain::Real);
barrier.wait();
let mut out = vec![ExprId(u32::MAX); N as usize];
let order: Vec<i64> = if t % 2 == 0 {
(0..N).collect()
} else {
(0..N).rev().collect()
};
for k in order {
let c = pool.integer(k);
let p = pool.pow(x, c);
let m = pool.mul(vec![c, p, y]);
let f = pool.func("sin", vec![m]);
out[k as usize] = pool.add(vec![f, p, pool.rational(k, 3)]);
}
out
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
for ids in &per_thread[1..] {
assert_eq!(ids, &per_thread[0], "threads disagree on an id");
}
assert_hash_consed(&pool);
}
#[test]
fn hits_during_concurrent_growth_are_stable() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let base: Vec<ExprId> = (0..500i64)
.map(|k| pool.add(vec![x, pool.integer(k)]))
.collect();
std::thread::scope(|s| {
for w in 0..4i64 {
let pool = &pool;
s.spawn(move || {
for k in 0..20_000i64 {
pool.integer(1_000_000 + w * 20_000 + k);
}
});
}
for _ in 0..4 {
let (pool, base) = (&pool, &base);
s.spawn(move || {
for round in 0..40 {
for (k, &id) in base.iter().enumerate() {
let again = pool.add(vec![x, pool.integer(k as i64)]);
assert_eq!(again, id, "round {round}: id moved");
}
}
});
}
});
assert_eq!(pool.len(), 1 + 2 * 500 + 80_000);
assert_hash_consed(&pool);
}
#[test]
fn a_panicking_intern_does_not_wedge_the_pool() {
const H: u64 = 0x1234_5678_9abc_def0;
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let bogus = ExprData::Pow {
base: x,
exp: ExprId(1_000_000),
};
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
intern_with_hash(&pool, H, bogus);
}));
assert!(r.is_err());
let before = pool.len();
let a = intern_with_hash(&pool, H, int(5));
assert_eq!(intern_with_hash(&pool, H, int(5)), a);
assert_eq!(pool.get(a), int(5));
assert_eq!(pool.len(), before + 1);
}
#[test]
fn ids_survive_index_growth() {
let pool = ExprPool::new();
let ids: Vec<ExprId> = (0..50_000i64).map(|k| pool.integer(k)).collect();
for (k, id) in ids.iter().enumerate() {
assert_eq!(pool.integer(k as i64), *id);
}
assert_eq!(pool.len(), 50_000);
}
}
#[cfg(test)]
mod intern_timing {
use super::*;
use crate::kernel::expr::{BigFloat, BigInt};
use rug::ops::Pow;
use std::hash::BuildHasher;
use std::time::Instant;
fn per_op(label: &str, iters: u32, mut f: impl FnMut()) {
for _ in 0..(iters / 10).max(1) {
f();
}
let t = Instant::now();
for _ in 0..iters {
f();
}
let ns = t.elapsed().as_nanos() as f64 / f64::from(iters);
println!("{label:<40} {ns:>12.1} ns/op");
}
#[test]
#[ignore]
fn number_atom_hash_and_intern_timings() {
let rs = std::collections::hash_map::RandomState::new();
let small = BigInt(rug::Integer::from(123_456_789));
let big = BigInt(rug::Integer::from(3).pow(2000));
let huge = rug::Integer::from(1) << 100_000u32;
let flt = BigFloat {
inner: rug::Float::with_val(53, 1.1),
prec: 53,
};
per_op("hash BigInt small", 1_000_000, || {
std::hint::black_box(rs.hash_one(std::hint::black_box(&small)));
});
per_op("hash BigInt 3^2000", 100_000, || {
std::hint::black_box(rs.hash_one(std::hint::black_box(&big)));
});
per_op("hash BigFloat 53-bit", 1_000_000, || {
std::hint::black_box(rs.hash_one(std::hint::black_box(&flt)));
});
let p = ExprPool::new();
for k in 0..1000i64 {
p.integer(k);
}
let mut k = 0i64;
per_op("pool.integer(k) hit", 1_000_000, || {
k = (k + 1) % 1000;
std::hint::black_box(p.integer(k));
});
per_op("pool.rational(1,3) hit", 1_000_000, || {
std::hint::black_box(p.rational(1, 3));
});
per_op("pool.float(1.1,53) hit", 1_000_000, || {
std::hint::black_box(p.float(1.1, 53));
});
p.integer(huge.clone());
per_op("pool.integer(2^100000) hit", 10_000, || {
std::hint::black_box(p.integer(huge.clone()));
});
let fresh = ExprPool::new();
let mut n = 0i64;
per_op("pool.integer miss (fresh values)", 1_000_000, || {
n += 1;
std::hint::black_box(fresh.integer(n));
});
let x = fresh.symbol("x", crate::kernel::Domain::Real);
let mut m = 0i64;
per_op("pool.add([x, k]) miss", 500_000, || {
m += 1;
let c = fresh.integer(m);
std::hint::black_box(fresh.add(vec![x, c]));
});
}
}