use crate::ball::IntervalEval;
use crate::kernel::expr::PredicateKind;
use crate::kernel::{ExprData, ExprId, ExprPool};
use rug::float::Round;
use rug::{Float, Integer, Rational};
use std::cmp::Ordering;
pub fn integer_to_f64(n: &Integer) -> f64 {
if n.significant_bits() <= f64::MANTISSA_DIGITS {
return n.to_f64();
}
let (mut f, dir) = Float::with_val_round(f64::MANTISSA_DIGITS, n, Round::Nearest);
f.subnormalize_ieee_round(dir, Round::Nearest);
f.to_f64()
}
pub fn rational_to_f64(r: &Rational) -> f64 {
let (numer, denom) = (r.numer(), r.denom());
if numer.significant_bits() <= f64::MANTISSA_DIGITS
&& denom.significant_bits() <= f64::MANTISSA_DIGITS
{
return numer.to_f64() / denom.to_f64();
}
let (mut f, dir) = Float::with_val_round(f64::MANTISSA_DIGITS, r, Round::Nearest);
f.subnormalize_ieee_round(dir, Round::Nearest);
f.to_f64()
}
const EXACT_INTEGER_F64_LIMIT: f64 = 9_007_199_254_740_992.0;
fn exact_integer_node(data: &ExprData) -> Option<&Integer> {
match data {
ExprData::Integer(n) => Some(&n.0),
ExprData::Rational(r) if *r.0.denom() == 1 => Some(r.0.numer()),
_ => None,
}
}
pub fn integer_is_exact_f64(n: &Integer) -> bool {
n.significant_bits() <= f64::MANTISSA_DIGITS
|| Integer::from_f64(integer_to_f64(n)).as_ref() == Some(n)
}
#[inline]
pub fn pow_f64(base: f64, exp: f64, exp_node: impl FnOnce() -> Option<ExprData>) -> f64 {
if exp.abs() < EXACT_INTEGER_F64_LIMIT {
return base.powf(exp);
}
pow_f64_wide_exponent(base, exp, exp_node())
}
#[cold]
fn pow_f64_wide_exponent(base: f64, exp: f64, exp_node: Option<ExprData>) -> f64 {
match exp_node.as_ref().and_then(exact_integer_node) {
Some(n) => pow_f64_integer_exponent(base, n),
None => base.powf(exp),
}
}
pub fn pow_f64_integer_exponent(base: f64, n: &Integer) -> f64 {
let exp = integer_to_f64(n);
if !base.is_finite() || !base.is_sign_negative() {
return base.powf(exp);
}
let magnitude = (-base).powf(exp);
if n.is_odd() {
-magnitude
} else {
magnitude
}
}
pub fn try_expr_f64(expr: ExprId, pool: &ExprPool) -> Option<f64> {
match pool.get(expr) {
ExprData::Integer(n) => Some(integer_to_f64(&n.0)),
ExprData::Rational(r) => Some(rational_to_f64(&r.0)),
ExprData::Float(f) => Some(f.inner.to_f64()),
_ => None,
}
}
pub fn try_predicate_bool(kind: &PredicateKind, args: &[ExprId], pool: &ExprPool) -> Option<bool> {
match kind {
PredicateKind::True => Some(true),
PredicateKind::False => Some(false),
PredicateKind::Not => {
let inner = try_predicate_bool_from_expr(args[0], pool)?;
Some(!inner)
}
PredicateKind::And => {
for &a in args {
if !try_predicate_bool_from_expr(a, pool)? {
return Some(false);
}
}
Some(true)
}
PredicateKind::Or => {
for &a in args {
if try_predicate_bool_from_expr(a, pool)? {
return Some(true);
}
}
Some(false)
}
PredicateKind::Lt
| PredicateKind::Le
| PredicateKind::Gt
| PredicateKind::Ge
| PredicateKind::Eq
| PredicateKind::Ne => {
let [lhs, rhs] = args else {
return None;
};
match exact_numeric_cmp(*lhs, *rhs, pool) {
Some(ord) => Some(decide_ordering(kind, ord)),
None => ball_decide(kind, *lhs, *rhs, pool),
}
}
}
}
fn decide_ordering(kind: &PredicateKind, ord: Option<Ordering>) -> bool {
match (kind, ord) {
(PredicateKind::Ne, None) => true,
(_, None) => false,
(PredicateKind::Lt, Some(o)) => o == Ordering::Less,
(PredicateKind::Le, Some(o)) => o != Ordering::Greater,
(PredicateKind::Gt, Some(o)) => o == Ordering::Greater,
(PredicateKind::Ge, Some(o)) => o != Ordering::Less,
(PredicateKind::Eq, Some(o)) => o == Ordering::Equal,
(PredicateKind::Ne, Some(o)) => o != Ordering::Equal,
_ => unreachable!("only comparisons reach decide_ordering"),
}
}
enum ExactNumber {
Rational(Rational),
Float(Float),
}
fn exact_number(expr: ExprId, pool: &ExprPool) -> Option<ExactNumber> {
match pool.get(expr) {
ExprData::Integer(n) => Some(ExactNumber::Rational(Rational::from(n.0))),
ExprData::Rational(r) => Some(ExactNumber::Rational(r.0)),
ExprData::Float(f) => Some(ExactNumber::Float(f.inner)),
_ => None,
}
}
fn exact_numeric_cmp(a: ExprId, b: ExprId, pool: &ExprPool) -> Option<Option<Ordering>> {
let (a, b) = (exact_number(a, pool)?, exact_number(b, pool)?);
Some(match (&a, &b) {
(ExactNumber::Rational(x), ExactNumber::Rational(y)) => Some(x.cmp(y)),
(ExactNumber::Rational(x), ExactNumber::Float(y)) => {
y.partial_cmp(x).map(Ordering::reverse)
}
(ExactNumber::Float(x), ExactNumber::Rational(y)) => x.partial_cmp(y),
(ExactNumber::Float(x), ExactNumber::Float(y)) => x.partial_cmp(y),
})
}
const BALL_PRECISIONS: [u32; 2] = [128, 512];
fn ball_decide(kind: &PredicateKind, a: ExprId, b: ExprId, pool: &ExprPool) -> Option<bool> {
for prec in BALL_PRECISIONS {
let ev = IntervalEval::new(prec);
let (x, y) = (ev.eval(a, pool)?, ev.eval(b, pool)?);
let (x_lo, x_hi, y_lo, y_hi) = (x.lo(), x.hi(), y.lo(), y.hi());
let below = x_hi < y_lo;
let above = x_lo > y_hi;
let same_point = x.is_exact() && y.is_exact() && x.mid == y.mid;
let decided = match kind {
PredicateKind::Lt if below => Some(true),
PredicateKind::Lt if above || same_point => Some(false),
PredicateKind::Le if below || same_point => Some(true),
PredicateKind::Le if above => Some(false),
PredicateKind::Gt if above => Some(true),
PredicateKind::Gt if below || same_point => Some(false),
PredicateKind::Ge if above || same_point => Some(true),
PredicateKind::Ge if below => Some(false),
PredicateKind::Eq if same_point => Some(true),
PredicateKind::Eq if below || above => Some(false),
PredicateKind::Ne if below || above => Some(true),
PredicateKind::Ne if same_point => Some(false),
_ => None,
};
if decided.is_some() {
return decided;
}
}
None
}
pub fn try_predicate_bool_from_expr(expr: ExprId, pool: &ExprPool) -> Option<bool> {
match pool.get(expr) {
ExprData::Predicate { kind, args } => try_predicate_bool(&kind, &args, pool),
_ => None,
}
}
#[cfg(test)]
mod conversion_tests {
use super::*;
use rug::ops::Pow;
#[test]
fn integer_to_f64_rounds_to_nearest_not_towards_zero() {
let n = Integer::from(10).pow(30);
assert_eq!(n.to_f64(), 9.999999999999999e29, "rug still truncates");
assert_eq!(integer_to_f64(&n), 1e30);
let n = Integer::from(2u64).pow(70) - 1u32;
assert_eq!(integer_to_f64(&n), 1.1805916207174113e21);
let n: Integer = "12345678901234567890123456789".parse().unwrap();
assert_eq!(integer_to_f64(&n), 1.2345678901234568e28);
assert_eq!(integer_to_f64(&-n.clone()), -1.2345678901234568e28);
for k in [0i64, 1, -1, 42, -7, 1 << 52, -(1 << 52)] {
assert_eq!(integer_to_f64(&Integer::from(k)), k as f64, "k = {k}");
}
let n = Integer::from(1u64 << 53) + 1u32;
assert_eq!(integer_to_f64(&n), 9007199254740992.0);
}
#[test]
fn integer_to_f64_saturates_past_dbl_max() {
let huge = Integer::from(10).pow(400);
assert_eq!(integer_to_f64(&huge), f64::INFINITY);
assert_eq!(integer_to_f64(&-huge.clone()), f64::NEG_INFINITY);
}
#[test]
fn rational_to_f64_rounds_to_nearest_not_towards_zero() {
let r = Rational::from((2, 5));
assert_eq!(r.to_f64(), 0.39999999999999997, "rug still truncates");
assert_eq!(rational_to_f64(&r), 0.4);
for (n, d, want) in [
(-7i64, 3i64, -2.3333333333333335f64),
(7, 3, 2.3333333333333335),
(-5, 3, -1.6666666666666667),
(355, 113, 3.1415929203539825),
(1, 3, 0.3333333333333333),
(22, 7, 3.142857142857143),
] {
assert_eq!(rational_to_f64(&Rational::from((n, d))), want, "{n}/{d}");
}
}
fn odd_wide_exponent() -> Integer {
Integer::from(10).pow(30) + 1u32
}
#[test]
fn pow_f64_keeps_the_parity_of_an_exponent_f64_cannot_hold() {
let n = odd_wide_exponent();
assert_eq!(integer_to_f64(&n), 1e30);
assert_eq!((-1.0f64).powf(1e30), 1.0, "the defect");
assert_eq!(pow_f64_integer_exponent(-1.0, &n), -1.0);
assert_eq!(pow_f64_integer_exponent(-1.0, &(n.clone() - 1u32)), 1.0);
assert_eq!(pow_f64_integer_exponent(1.0, &n), 1.0);
let node = ExprData::Integer(crate::kernel::BigInt(n.clone()));
assert_eq!(pow_f64(-1.0, 1e30, || Some(node.clone())), -1.0);
}
#[test]
fn pow_f64_leaves_every_representable_exponent_bit_for_bit_alone() {
for &base in &[-3.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 7.25] {
for &e in &[0.0, 1.0, 2.0, 3.0, -1.0, -2.0, 0.5, -0.5, 53.0, 1e15, -1e15] {
let n = Integer::from(e as i64);
let node = ExprData::Integer(crate::kernel::BigInt(n));
let got = pow_f64(base, e, || Some(node));
let want = base.powf(e);
assert_eq!(
got.to_bits(),
want.to_bits(),
"{base} ^ {e}: {got} vs {want}"
);
}
}
}
#[test]
fn pow_f64_leaves_an_exactly_representable_wide_exponent_alone() {
let n = Integer::from(1u64 << 60);
assert_eq!(pow_f64_integer_exponent(-1.0, &n), 1.0);
assert_eq!((-1.0f64).powf(integer_to_f64(&n)), 1.0);
let odd = n + 1u32;
assert_eq!(integer_to_f64(&odd), 1.152921504606847e18);
assert_eq!(pow_f64_integer_exponent(-1.0, &odd), -1.0);
}
#[test]
fn pow_f64_signs_the_overflow_and_the_underflow() {
let n = odd_wide_exponent();
assert_eq!(pow_f64_integer_exponent(-2.0, &n), f64::NEG_INFINITY);
assert_eq!(pow_f64_integer_exponent(2.0, &n), f64::INFINITY);
assert!(pow_f64_integer_exponent(-0.5, &n).is_sign_negative());
assert_eq!(pow_f64_integer_exponent(-0.5, &n), 0.0);
assert!(pow_f64_integer_exponent(-0.0, &n).is_sign_negative());
}
#[test]
fn pow_f64_falls_back_for_an_exponent_that_was_never_exact() {
let node = ExprData::Float(crate::kernel::BigFloat {
inner: rug::Float::with_val(53, 1e30),
prec: 53,
});
assert_eq!(pow_f64(-1.0, 1e30, || Some(node)), (-1.0f64).powf(1e30));
assert_eq!(pow_f64(-1.0, 1e30, || None), (-1.0f64).powf(1e30));
assert!(pow_f64(-2.0, f64::NAN, || None).is_nan());
}
#[test]
fn rational_to_f64_survives_numerator_and_denominator_both_overflowing() {
let numer = Integer::from(3) * Integer::from(10).pow(400) + 1u32;
let denom = Integer::from(2) * Integer::from(10).pow(400);
assert!(numer.to_f64().is_infinite() && denom.to_f64().is_infinite());
assert!(
(numer.to_f64() / denom.to_f64()).is_nan(),
"the old formula"
);
let r = Rational::from((numer, denom));
assert_eq!(rational_to_f64(&r), 1.5);
}
#[test]
fn rational_to_f64_handles_subnormals_and_overflow() {
let big = Integer::from(10).pow(400);
assert_eq!(rational_to_f64(&Rational::from((1, big.clone()))), 0.0);
assert_eq!(rational_to_f64(&Rational::from((big, 1))), f64::INFINITY);
let denom = Integer::from(2u32).pow(1074);
assert_eq!(rational_to_f64(&Rational::from((1, denom))), 5e-324);
}
}
#[cfg(test)]
mod exact_predicate_tests {
use super::*;
use crate::kernel::subs::{fold_predicates, subs};
use crate::kernel::Domain;
use rug::ops::Pow;
use std::collections::HashMap;
fn big(e: u32) -> Integer {
Integer::from(10).pow(e)
}
fn decide(p: &ExprPool, pred: ExprId) -> Option<bool> {
try_predicate_bool_from_expr(pred, p)
}
#[test]
fn integers_past_f64_compare_exactly() {
let p = ExprPool::new();
let a = p.integer(big(30));
let b = p.integer(big(30) + 1u32);
assert_eq!(decide(&p, p.pred_eq(b, a)), Some(false));
assert_eq!(decide(&p, p.pred_ne(b, a)), Some(true));
assert_eq!(decide(&p, p.pred_lt(a, b)), Some(true));
assert_eq!(decide(&p, p.pred_le(b, a)), Some(false));
assert_eq!(decide(&p, p.pred_gt(b, a)), Some(true));
assert_eq!(decide(&p, p.pred_ge(a, b)), Some(false));
let two53 = p.integer(Integer::from(1u64 << 53));
let two53p1 = p.integer(Integer::from((1u64 << 53) + 1));
assert_eq!(decide(&p, p.pred_gt(two53p1, two53)), Some(true));
}
#[test]
fn rationals_compare_exactly() {
let p = ExprPool::new();
let third = p.rational(1, 3);
let approx = p.rational(3_333_333_333_333_333_i64, big(16));
assert_eq!(decide(&p, p.pred_ne(approx, third)), Some(true));
assert_eq!(decide(&p, p.pred_eq(approx, third)), Some(false));
assert_eq!(decide(&p, p.pred_lt(approx, third)), Some(true));
let tiny = p.rational(1, big(400));
let zero = p.integer(0);
assert_eq!(decide(&p, p.pred_eq(zero, tiny)), Some(false));
assert_eq!(decide(&p, p.pred_lt(zero, tiny)), Some(true));
let h1 = p.rational(big(400) + 1u32, 3);
let h2 = p.rational(big(400) + 2u32, 3);
assert_eq!(decide(&p, p.pred_lt(h1, h2)), Some(true));
}
#[test]
fn floats_compare_as_the_dyadic_rationals_they_are() {
let p = ExprPool::new();
let f = p.float(1.0 / 3.0, 53);
let third = p.rational(1, 3);
assert_eq!(decide(&p, p.pred_lt(f, third)), Some(true));
assert_eq!(decide(&p, p.pred_eq(f, third)), Some(false));
assert_eq!(decide(&p, p.pred_gt(third, f)), Some(true));
let half = p.float(0.5, 53);
assert_eq!(decide(&p, p.pred_eq(half, p.rational(1, 2))), Some(true));
let nan = p.float(f64::NAN, 53);
let one = p.integer(1);
assert_eq!(decide(&p, p.pred_eq(nan, one)), Some(false));
assert_eq!(decide(&p, p.pred_ne(nan, one)), Some(true));
assert_eq!(decide(&p, p.pred_lt(one, nan)), Some(false));
}
#[test]
fn subs_folds_exact_comparisons_and_picks_the_right_branch() {
let p = ExprPool::new();
let x = p.symbol("x", Domain::Real);
let at = |e: ExprId, v: Integer| {
let mut m = HashMap::new();
m.insert(x, p.integer(v));
fold_predicates(subs(e, &m, &p), &p)
};
let t = p.pred_true();
let f = p.pred_false();
assert_eq!(at(p.pred_eq(x, p.integer(big(30))), big(30) + 1u32), f);
assert_eq!(at(p.pred_lt(x, p.integer(big(30) + 1u32)), big(30)), t);
let two53 = Integer::from(1u64 << 53);
let pw = p.piecewise(
vec![(p.pred_gt(x, p.integer(two53.clone())), p.integer(1))],
p.integer(0),
);
assert_eq!(at(pw, two53 + 1u32), p.integer(1));
}
#[test]
fn closed_constants_are_decided_by_enclosure() {
let p = ExprPool::new();
let sqrt2 = p.pow(p.integer(2), p.rational(1, 2));
let one = p.integer(1);
assert_eq!(decide(&p, p.pred_gt(sqrt2, one)), Some(true));
assert_eq!(decide(&p, p.pred_eq(sqrt2, one)), Some(false));
let pi = p.symbol("pi", Domain::Real);
let approx = p.rational(355, 113);
assert_eq!(decide(&p, p.pred_lt(pi, approx)), Some(true));
assert_eq!(decide(&p, p.pred_ne(pi, approx)), Some(true));
let close = p.add(vec![one, p.rational(1, Integer::from(2).pow(200))]);
assert_eq!(decide(&p, p.pred_gt(close, one)), Some(true));
}
#[test]
fn an_overlap_is_left_undecided_rather_than_guessed() {
let p = ExprPool::new();
let two = p.integer(2);
let sq = p.pow(p.pow(two, p.rational(1, 2)), two);
assert_eq!(decide(&p, p.pred_eq(sq, two)), None);
assert_eq!(decide(&p, p.pred_lt(sq, two)), None);
assert_eq!(decide(&p, p.pred_ge(sq, two)), None);
let x = p.symbol("x", Domain::Real);
assert_eq!(decide(&p, p.pred_gt(x, two)), None);
}
#[test]
fn a_sum_past_the_working_precision_is_decided_on_retry() {
let p = ExprPool::new();
let a = p.integer(big(50));
let b = p.add(vec![p.integer(big(50)), p.integer(1)]);
assert_eq!(decide(&p, p.pred_eq(a, b)), Some(false));
assert_eq!(decide(&p, p.pred_lt(a, b)), Some(true));
}
}