568f52165d
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
190 lines
6.4 KiB
Rust
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()
|
|
}
|