Files
improvise/crates/numtower/tests/oracle/mod.rs
T
Claude (numtower) 568f52165d fix(numtower): inexact presence per value, POW sign from exact parity, i128::MIN numerators (improvise-59nw.1)
Answers ChatGPT Codex's P1 and Copilot's two findings on PR #337; each has a regression test that failed at edbb50c4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
2026-10-08 22:47:04 -07:00

190 lines
6.4 KiB
Rust

//! The `BigRational` oracle and the generators the property tests share.
//! Each property is a checker taking the implementation, so a rival can be run through it.
#![allow(dead_code)]
use std::cmp::Ordering;
use num_bigint::BigInt;
use num_rational::BigRational;
use num_traits::{One, Signed, ToPrimitive, Zero};
use numtower::{Decimal, Num, NumRef};
use proptest::prelude::*;
use proptest::test_runner::{Config, RngAlgorithm, TestCaseError, TestRng, TestRunner};
pub fn q(n: &Num) -> Option<BigRational> {
match n.kind() {
NumRef::Exact(d) => Some(BigRational::new(
d.mantissa().into(),
BigInt::from(10).pow(d.scale()),
)),
NumRef::Fraction(r) => Some(BigRational::new((*r.numer()).into(), (*r.denom()).into())),
NumRef::Inexact(x) => BigRational::from_float(x),
}
}
/// Whether `v` fits a stored exact arm: a decimal at scale <= 28, or an `i128` fraction.
pub fn fits(v: &BigRational) -> bool {
let limit = BigInt::one() << 96;
let decimal = (0..=28u32).any(|s| {
let scaled = v * BigRational::from_integer(BigInt::from(10).pow(s));
scaled.is_integer() && scaled.to_integer().abs() < limit
});
let (min, max) = (BigInt::from(i128::MIN), BigInt::from(i128::MAX));
decimal || (&min <= v.numer() && v.numer() <= &max && v.denom() <= &max)
}
/// Whether `f` is the round-to-nearest-even f64 of `v`: the review's interval form.
pub fn is_nearest(f: f64, v: &BigRational) -> bool {
let exact = |x: f64| BigRational::from_float(x).expect("finite");
let two = BigRational::from_integer(BigInt::from(2));
let half_ulp_past = |x: f64, up: bool| {
let (next, inward) = match up {
true => (x.next_up(), x.next_down()),
false => (x.next_down(), x.next_up()),
};
match next.is_finite() {
true => (exact(x) + exact(next)) / &two,
false => exact(x) + (exact(x) - exact(inward)) / &two,
}
};
match f.is_finite() {
false => {
let edge = half_ulp_past(f64::MAX.copysign(f), f > 0.0);
(f > 0.0 && v >= &edge) || (f < 0.0 && v <= &edge)
}
true => {
let lo = half_ulp_past(f, false);
let hi = half_ulp_past(f, true);
let even = f.to_bits() & 1 == 0;
match even {
true => &lo <= v && v <= &hi,
false => &lo < v && v < &hi,
}
}
}
}
/// A decimal's text built digit by digit, without `rust_decimal`'s `Display`.
pub fn decimal_text(mantissa: i128, scale: u32) -> String {
let digits = format!(
"{:0>width$}",
mantissa.unsigned_abs(),
width = scale as usize + 1
);
let (int, frac) = digits.split_at(digits.len() - scale as usize);
let sign = if mantissa < 0 { "-" } else { "" };
format!("{sign}{int}.{frac}0")
}
/// The correctly rounded f64 of an exact value, independent of `numtower`'s own.
pub fn independent_f64(n: &Num) -> f64 {
match n.kind() {
NumRef::Exact(d) => decimal_text(d.mantissa(), d.scale()).parse().unwrap(),
NumRef::Fraction(_) => q(n).unwrap().to_f64().unwrap(),
NumRef::Inexact(x) => x,
}
}
/// Same arm, same value and same scale: what a fingerprint would see.
pub fn same(a: &Num, b: &Num) -> bool {
match (a.kind(), b.kind()) {
(NumRef::Exact(x), NumRef::Exact(y)) => {
x.mantissa() == y.mantissa() && x.scale() == y.scale()
}
(NumRef::Fraction(x), NumRef::Fraction(y)) => x == y,
(NumRef::Inexact(x), NumRef::Inexact(y)) => x.to_bits() == y.to_bits(),
_ => false,
}
}
pub fn arm(n: &Num) -> &'static str {
match n.kind() {
NumRef::Exact(_) => "exact",
NumRef::Fraction(_) => "fraction",
NumRef::Inexact(_) => "inexact",
}
}
pub fn mantissa96() -> impl Strategy<Value = i128> {
let max = (1i128 << 96) - 1;
prop_oneof![
-max..=max,
-1_000_000i128..1_000_000,
-(10i128.pow(15))..10i128.pow(15),
]
}
pub fn decimal() -> impl Strategy<Value = Num> {
(mantissa96(), 0u32..=28).prop_map(|(m, s)| Num::exact(Decimal::from_i128_with_scale(m, s)))
}
fn of_width(bits: u32) -> impl Strategy<Value = i128> {
(any::<u128>(), any::<bool>()).prop_map(move |(r, neg)| {
let v = ((r >> (128 - bits)) | (1u128 << (bits - 1))) as i128;
if neg { -v } else { v }
})
}
pub fn fraction() -> impl Strategy<Value = Num> {
let widths = (1u32..=126, 1u32..=126)
.prop_flat_map(|(a, b)| (of_width(a), of_width(b)))
.prop_map(|(n, d)| Num::ratio(n, d).unwrap());
let edge = (1i128..i128::MAX).prop_map(|d| Num::ratio(i128::MIN, d | 1).unwrap());
prop_oneof![8 => widths, 1 => edge]
}
pub fn finite_f64() -> impl Strategy<Value = f64> {
use proptest::num::f64::{NEGATIVE, NORMAL, POSITIVE, SUBNORMAL, ZERO};
prop_oneof![
(POSITIVE | NEGATIVE | NORMAL | SUBNORMAL | ZERO),
-1e6f64..1e6,
(-1000i64..1000).prop_map(|k| k as f64 / 8.0),
]
}
pub fn exact() -> impl Strategy<Value = Num> {
prop_oneof![decimal(), fraction()]
}
pub fn any_num() -> impl Strategy<Value = Num> {
prop_oneof![
3 => exact(),
1 => finite_f64().prop_map(Num::inexact),
1 => prop_oneof![Just(f64::NAN), Just(f64::INFINITY), Just(f64::NEG_INFINITY)].prop_map(Num::inexact),
]
}
/// Runs a property; `deterministic` for a rival, so its failure is not luck.
pub fn run<S: Strategy>(
strategy: S,
cases: u32,
deterministic: bool,
test: impl Fn(S::Value) -> Result<(), TestCaseError>,
) -> Result<(), String> {
let config = Config {
cases,
failure_persistence: None,
..Config::default()
};
let mut runner = match deterministic {
true => TestRunner::new_with_rng(config, TestRng::deterministic_rng(RngAlgorithm::ChaCha)),
false => TestRunner::new(config),
};
runner.run(&strategy, test).map_err(|e| e.to_string())
}
/// A rival must fail on the property's own assertion, never on something else.
pub fn assert_caught(result: Result<(), String>, marker: &str) {
let err = result.expect_err("the rival passed: the property cannot see it");
assert!(err.contains(marker), "failed for another reason: {err}");
}
pub fn oracle_cmp(a: &Num, b: &Num) -> Option<Ordering> {
Some(q(a)?.cmp(&q(b)?))
}
pub fn integer(v: &BigRational) -> bool {
v.is_integer() && !v.numer().is_zero()
}