use crate::kernel::domain::Domain;
use std::fmt;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExprId(pub(crate) u32);
#[derive(Debug, Clone)]
pub struct BigInt(pub rug::Integer);
impl PartialEq for BigInt {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for BigInt {}
impl Hash for BigInt {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl fmt::Display for BigInt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct BigRat(pub rug::Rational);
impl PartialEq for BigRat {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for BigRat {}
impl Hash for BigRat {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl fmt::Display for BigRat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if *self.0.denom() == 1 {
write!(f, "{}", self.0.numer())
} else {
write!(f, "{}/{}", self.0.numer(), self.0.denom())
}
}
}
#[derive(Debug, Clone)]
pub struct BigFloat {
pub inner: rug::Float,
pub prec: u32,
}
impl PartialEq for BigFloat {
fn eq(&self, other: &Self) -> bool {
if self.prec != other.prec {
return false;
}
match (self.inner.is_nan(), other.inner.is_nan()) {
(true, true) => true,
(false, false) => self.inner == other.inner,
_ => false,
}
}
}
impl Eq for BigFloat {}
impl Hash for BigFloat {
fn hash<H: Hasher>(&self, state: &mut H) {
self.prec.hash(state);
let f = &self.inner;
if f.is_nan() {
0u8.hash(state);
} else if f.is_zero() {
1u8.hash(state);
} else if f.is_infinite() {
2u8.hash(state);
f.is_sign_negative().hash(state);
} else {
3u8.hash(state);
f.is_sign_negative().hash(state);
f.get_exp().hash(state);
if let Some(sig) = f.get_significand() {
let limbs = sig.as_limbs();
let first = limbs.iter().position(|&l| l != 0).unwrap_or(limbs.len());
limbs[first..].hash(state);
}
}
}
}
impl fmt::Display for BigFloat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = self.inner.to_string();
if self.inner.is_finite() && !s.contains(['.', 'e', 'E']) {
write!(f, "{s}.0")
} else {
f.write_str(&s)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PredicateKind {
Lt,
Le,
Gt,
Ge,
Eq,
Ne,
And,
Or,
Not,
True,
False,
}
impl fmt::Display for PredicateKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
PredicateKind::Lt => "<",
PredicateKind::Le => "≤",
PredicateKind::Gt => ">",
PredicateKind::Ge => "≥",
PredicateKind::Eq => "=",
PredicateKind::Ne => "≠",
PredicateKind::And => "∧",
PredicateKind::Or => "∨",
PredicateKind::Not => "¬",
PredicateKind::True => "True",
PredicateKind::False => "False",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ExprData {
Symbol {
name: String,
domain: Domain,
commutative: bool,
},
Integer(BigInt),
Rational(BigRat),
Float(BigFloat),
Add(Vec<ExprId>),
Mul(Vec<ExprId>),
Pow {
base: ExprId,
exp: ExprId,
},
Func {
name: String,
args: Vec<ExprId>,
},
Piecewise {
branches: Vec<(ExprId /* cond */, ExprId /* value */)>,
default: ExprId,
},
Predicate {
kind: PredicateKind,
args: Vec<ExprId>,
},
Forall {
var: ExprId,
body: ExprId,
},
Exists {
var: ExprId,
body: ExprId,
},
BigO(ExprId),
RootSum {
poly: ExprId,
var: ExprId,
body: ExprId,
},
}
#[cfg(test)]
mod number_hash_tests {
use super::*;
use proptest::prelude::*;
use rug::{Assign, Float, Integer, Rational};
use std::collections::hash_map::RandomState;
use std::hash::BuildHasher;
fn h<T: Hash>(rs: &RandomState, x: &T) -> u64 {
rs.hash_one(x)
}
fn assert_consistent<T: Hash + Eq + fmt::Debug>(a: &T, b: &T) {
let rs = RandomState::new();
if a == b {
assert!(b == a, "Eq not symmetric: {a:?} vs {b:?}");
assert_eq!(
h(&rs, a),
h(&rs, b),
"equal values hash apart: {a:?} vs {b:?}"
);
}
}
fn float(inner_prec: u32, v: f64, prec: u32) -> BigFloat {
BigFloat {
inner: Float::with_val(inner_prec, v),
prec,
}
}
#[test]
fn integer_hash_ignores_allocation() {
let a = BigInt(Integer::from(-42));
let mut grown = Integer::with_capacity(10_000);
grown.assign(Integer::from(1) << 5000u32);
grown -= Integer::from(1) << 5000u32;
grown -= 42;
let b = BigInt(grown);
assert_eq!(a, b);
assert_consistent(&a, &b);
let z1 = BigInt(Integer::new());
let z2 = BigInt(Integer::from(7) - 7);
assert_eq!(z1, z2);
assert_consistent(&z1, &z2);
let rs = RandomState::new();
assert_ne!(
h(&rs, &BigInt(Integer::from(5))),
h(&rs, &BigInt(Integer::from(-5)))
);
}
#[test]
fn rational_hash_uses_canonical_form() {
let a = BigRat(Rational::from((6, -4)));
let b = BigRat(Rational::from((-3, 2)));
assert_eq!(a, b);
assert_consistent(&a, &b);
let c = BigRat(Rational::from((10, 5)));
let d = BigRat(Rational::from(2));
assert_eq!(c, d);
assert_consistent(&c, &d);
}
#[test]
fn float_hash_matches_eq_on_special_values() {
let pz = float(53, 0.0, 53);
let nz = float(53, -0.0, 53);
assert!(nz.inner.is_sign_negative());
assert_eq!(pz, nz);
assert_consistent(&pz, &nz);
let n1 = float(53, f64::NAN, 53);
let n2 = BigFloat {
inner: -Float::with_val(113, f64::NAN),
prec: 53,
};
assert_eq!(n1, n2);
assert_consistent(&n1, &n2);
assert_ne!(
float(53, f64::INFINITY, 53),
float(53, f64::NEG_INFINITY, 53)
);
assert_ne!(float(53, 1.0, 53), float(53, 1.0, 64));
}
#[test]
fn float_hash_ignores_inner_mpfr_precision() {
for v in [
1.0,
-1.5,
0.1,
1e300,
-3.0e-300,
f64::MIN_POSITIVE,
f64::MAX,
] {
for p in [53u32, 64, 65, 128, 129, 1000] {
let a = float(53, v, 53);
let b = float(p, v, 53);
assert_eq!(a, b, "{v} at inner prec {p}");
assert_consistent(&a, &b);
}
}
}
#[test]
fn hashes_spread_over_distinct_values() {
let rs = RandomState::new();
let mut seen = std::collections::HashSet::new();
for k in -500i64..500 {
seen.insert(h(&rs, &BigInt(Integer::from(k))));
seen.insert(h(&rs, &BigRat(Rational::from((k, 7)))));
seen.insert(h(&rs, &float(53, k as f64 * 0.37, 53)));
}
assert!(seen.len() >= 2990, "only {} distinct hashes", seen.len());
}
fn big_integer() -> impl Strategy<Value = Integer> {
prop_oneof![
any::<i64>().prop_map(Integer::from),
any::<i128>().prop_map(Integer::from),
(any::<bool>(), proptest::collection::vec(any::<u64>(), 0..6)).prop_map(
|(neg, limbs)| {
let v = Integer::from_digits(&limbs, rug::integer::Order::Lsf);
if neg {
-v
} else {
v
}
}
),
]
}
proptest! {
#[test]
fn prop_integer_eq_implies_hash(a in big_integer(), b in big_integer(), k in big_integer()) {
let mut round = Integer::with_capacity(4096);
round.assign(&a + &k);
round -= &k;
prop_assert_eq!(&a, &round);
assert_consistent(&BigInt(a.clone()), &BigInt(round));
assert_consistent(&BigInt(a), &BigInt(b));
}
#[test]
fn prop_rational_eq_implies_hash(
n in big_integer(),
d in big_integer(),
k in big_integer(),
) {
prop_assume!(d != 0 && k != 0);
let r = BigRat(Rational::from((n.clone(), d.clone())));
let scaled = BigRat(Rational::from((n * &k, d * &k)));
prop_assert_eq!(&r, &scaled);
assert_consistent(&r, &scaled);
}
#[test]
fn prop_float_eq_implies_hash(
a in any::<f64>(),
b in any::<f64>(),
pa in 2u32..300,
pb in 2u32..300,
prec in prop_oneof![Just(53u32), Just(64u32), 2u32..200],
) {
let x = BigFloat { inner: Float::with_val(pa, a), prec };
let y = BigFloat { inner: Float::with_val(pb, b), prec };
assert_consistent(&x, &y);
let wider = BigFloat {
inner: Float::with_val(pa.max(pb) + 64, &x.inner),
prec,
};
prop_assert_eq!(&x, &wider);
assert_consistent(&x, &wider);
}
#[test]
fn prop_distinct_floats_hash_apart(a in any::<f64>(), b in any::<f64>()) {
let x = float(53, a, 53);
let y = float(53, b, 53);
let rs = RandomState::new();
if x != y {
prop_assert_ne!(h(&rs, &x), h(&rs, &y));
}
}
}
}