Skip to content

Commit

Permalink
Update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
tgross35 committed Jul 15, 2024
1 parent 298b6de commit c451197
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 19 deletions.
46 changes: 34 additions & 12 deletions src/etc/test-float-parse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,40 @@
These are tests designed to test decimal to float conversions (`dec2flt`) used
by the standard library.

Breakdown:
The generators work as follows:

- Generators (implement the `Generator` trait) provide test cases
- We `.parse()` to the relevant float types and decompose it into its
significand and its exponent.
- Separately, we parse to an exact value with `BigRational`
- Check sig + exp against `BigRational` math to make sure it is accurate within
rounding, or correctly rounded to +/- inf.
- `rayon` is awesome so this all gets parallelized nicely
- Each generator is a struct that lives somewhere in the `gen` module. Usually
it is generic over a float type.
- These generators must implement `Iterator`, which should return a context
type that can be used to construct a test string (but usually not the string
itself).
- They must also implement the `Generator` trait, which provides a method to
write test context to a string as a test case, as well as some extra metadata.

The split between context generation and string construction is so that
we can reuse string allocations.
- Each generator gets registered once for each float type. All of these
generators then get iterated, and each test case checked against the float
type's parse implementation.

Some tests generate strings, others generate bit patterns. For those that
generate bit patterns, we need to use float -> dec conversions so that also
gets tested.
Some tests produce decimal strings, others generate bit patterns that need
to convert to the float type before printing to a string. For these, float to
decimal (`flt2dec`) conversions get tested, if unintentionally.

todo
For each test case, the following is done:

- The test string is parsed to the float type using the standard library's
implementation.
- The test string is parsed separately to a `BigRational`, which acts as a
representation with infinite precision.
- The rational value then gets checked that it is within the float's
representable values (absolute value greater than the smallest number to
round to zero, but less less than the first value to round to infinity). If
these limits are exceeded, check that the parsed float reflects that.
- For real nonzero numbers, the parsed float is converted into a
rational using `significand * 2^exponent`. It is then checked against the
actual rational value, and verified to be within half a bit's precision
of the parsed value.

This is all highly parallelized with `rayon`; test generators can run in
parallel, and their tests get chunked and run in parallel.
28 changes: 21 additions & 7 deletions src/etc/test-float-parse/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ pub struct Constants {
powers_of_two: BTreeMap<i32, BigRational>,
/// Half of each power of two. ULP = "unit in last position".
///
/// This is a mapping from integers to half of the precision that can be had at that
/// exponent. That is, these values are in between values that can be represented with a
/// certain exponent, assuming an integer significand (always true for float representations).
/// This is a mapping from integers to half the precision available at that exponent. In other
/// words, `0.5 * 2^n` = `2^(n-1)`, which is half the distance between `m * 2^n` and
/// `(m + 1) * 2^n`, m ∈ ℤ.
///
/// So, this is the maximum error from a real number to its floating point representation,
/// assuming the float type can represent the exponent.
half_ulp: BTreeMap<i32, BigRational>,
/// Handy to have around so we don't need to reallocate for it
two: BigInt,
Expand Down Expand Up @@ -176,14 +179,25 @@ impl<F: Float> FloatRes<F> {
/// Check that `sig * 2^exp` is the same as `rational`, within the float's error margin.
fn validate_real(rational: BigRational, sig: F::SInt, exp: i32) -> Result<(), CheckFailure> {
let consts = F::constants();
// Rational from the parsed value (`sig * 2^exp`). Use cached powers of two to be a bit
// faster.
let parsed_rational = consts.powers_of_two.get(&exp).unwrap() * sig.to_bigint().unwrap();

// `2^exp`. Use cached powers of two to be faster.
let two_exp = consts
.powers_of_two
.get(&exp)
.unwrap_or_else(|| panic!("missing exponent {exp} for {}", type_name::<F>()));

// Rational from the parsed value, `sig * 2^exp`
let parsed_rational = two_exp * sig.to_bigint().unwrap();
let error = (parsed_rational - rational).abs();

// Determine acceptable error at this exponent
// Determine acceptable error at this exponent, which is halfway between this value
// (`sig * 2^exp`) and the next value up (`(sig+1) * 2^exp`).
let half_ulp = consts.half_ulp.get(&exp).unwrap();

// Our real value is allowed to be exactly at the midpoint between two values, or closer
// to the real value.
// todo: this currently allows rounding either up or down at the midpoint.
// Is this correct?
if &error <= half_ulp {
return Ok(());
}
Expand Down

0 comments on commit c451197

Please sign in to comment.