Skip to content

Commit 8d4f400

Browse files
committed
Check for rounding at the centerpoint
1 parent b46620d commit 8d4f400

File tree

3 files changed

+68
-24
lines changed

3 files changed

+68
-24
lines changed

src/etc/test-float-parse/README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
These are tests designed to test decimal to float conversions (`dec2flt`) used
44
by the standard library.
55

6+
It consistes of a collection of test generators that each generate a set of
7+
patterns intended to test a specific property. In addition, there are exhaustive
8+
tests (for <= `f32`) and fuzzers (for anything that can't be run exhaustively).
9+
610
The generators work as follows:
711

812
- Each generator is a struct that lives somewhere in the `gen` module. Usually
@@ -15,13 +19,14 @@ The generators work as follows:
1519

1620
The split between context generation and string construction is so that we can
1721
reuse string allocations.
18-
- Each generator gets registered once for each float type. All of these
19-
generators then get iterated, and each test case checked against the float
20-
type's parse implementation.
22+
- Each generator gets registered once for each float type. Each of these
23+
generators then get their iterator called, and each test case checked against
24+
the float type's parse implementation.
2125

22-
Some tests produce decimal strings, others generate bit patterns that need to
23-
convert to the float type before printing to a string. For these, float to
24-
decimal (`flt2dec`) conversions get tested, if unintentionally.
26+
Some generators produce decimal strings, others create bit patterns that need to
27+
be bitcasted to the float type, which then uses its `Display` implementation to
28+
write to a string. For these, float to decimal (`flt2dec`) conversions also get
29+
tested, if unintentionally.
2530

2631
For each test case, the following is done:
2732

@@ -36,6 +41,7 @@ For each test case, the following is done:
3641
- For real nonzero numbers, the parsed float is converted into a rational using
3742
`significand * 2^exponent`. It is then checked against the actual rational
3843
value, and verified to be within half a bit's precision of the parsed value.
44+
Also it is checked that ties round to even.
3945

4046
This is all highly parallelized with `rayon`; test generators can run in
4147
parallel, and their tests get chunked and run in parallel.

src/etc/test-float-parse/src/lib.rs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub const DEFAULT_FUZZ_COUNT: u64 = u32::MAX as u64;
3939
const HUGE_TEST_CUTOFF: u64 = 5_000_000;
4040

4141
/// Seed for tests that use a deterministic RNG.
42-
pub const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
42+
const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
4343

4444
/// Global configuration
4545
#[derive(Debug)]
@@ -236,9 +236,12 @@ impl Msg {
236236
pb.set_message(format! {"{failures}"});
237237
pb.set_position(executed);
238238
}
239-
Update::Failure { fail, input } => {
240-
mp.println(format!("Failure in '{}': {fail}. parsing '{input}'", test.name))
241-
.unwrap();
239+
Update::Failure { fail, input, float_res } => {
240+
mp.println(format!(
241+
"Failure in '{}': {fail}. parsing '{input}'. Parsed as: {float_res}",
242+
test.name
243+
))
244+
.unwrap();
242245
}
243246
Update::Completed(c) => {
244247
test.finalize_pb(&c);
@@ -265,11 +268,17 @@ impl Msg {
265268
enum Update {
266269
/// Starting a new test runner.
267270
Started,
268-
/// Completed a out of b tests
271+
/// Completed a out of b tests.
269272
Progress { executed: u64, failures: u64 },
270-
/// Received a failed test
271-
Failure { fail: CheckFailure, input: Box<str> },
272-
/// Exited with an unexpected condition
273+
/// Received a failed test.
274+
Failure {
275+
fail: CheckFailure,
276+
/// String for which parsing was attempted.
277+
input: Box<str>,
278+
/// The parsed & decomposed `FloatRes`, aleady stringified so we don't need generics here.
279+
float_res: Box<str>,
280+
},
281+
/// Exited with an unexpected condition.
273282
Completed(Completed),
274283
}
275284

@@ -296,6 +305,9 @@ enum CheckFailure {
296305
error_float: Option<f64>,
297306
/// Error as a rational string (since it can't always be represented as a float).
298307
error_str: Box<str>,
308+
/// True if the error was caused by not rounding to even at the midpoint between
309+
/// two representable values.
310+
incorrect_midpoint_rounding: bool,
299311
},
300312
}
301313

@@ -315,8 +327,17 @@ impl fmt::Display for CheckFailure {
315327
CheckFailure::ExpectedNan => write!(f, "expected a NaN but did not get it"),
316328
CheckFailure::ExpectedInf => write!(f, "expected +inf but did not get it"),
317329
CheckFailure::ExpectedNegInf => write!(f, "expected -inf but did not get it"),
318-
CheckFailure::InvalidReal { error_float, error_str } => {
319-
write!(f, "real number did not parse correctly; error:{error_str}")?;
330+
CheckFailure::InvalidReal { error_float, error_str, incorrect_midpoint_rounding } => {
331+
if *incorrect_midpoint_rounding {
332+
write!(
333+
f,
334+
"midpoint between two representable values did not correctly \
335+
round to even; error: {error_str}"
336+
)?;
337+
} else {
338+
write!(f, "real number did not parse correctly; error: {error_str}")?;
339+
}
340+
320341
if let Some(float) = error_float {
321342
write!(f, " ({float})")?;
322343
}

src/etc/test-float-parse/src/validate.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ pub fn validate<F: Float>(input: &str, allow_nan: bool) -> Result<(), Update> {
106106

107107
/// The result of parsing a string to a float type.
108108
#[derive(Clone, Copy, Debug, PartialEq)]
109-
enum FloatRes<F: Float> {
109+
pub enum FloatRes<F: Float> {
110110
Inf,
111111
NegInf,
112112
Zero,
@@ -173,7 +173,11 @@ impl<F: Float> FloatRes<F> {
173173
(Rational::Finite(r), FloatRes::Real { sig, exp }) => Self::validate_real(r, sig, exp),
174174
};
175175

176-
res.map_err(|fail| Update::Failure { fail, input: input.into() })
176+
res.map_err(|fail| Update::Failure {
177+
fail,
178+
input: input.into(),
179+
float_res: format!("{self:?}").into(),
180+
})
177181
}
178182

179183
/// Check that `sig * 2^exp` is the same as `rational`, within the float's error margin.
@@ -188,20 +192,32 @@ impl<F: Float> FloatRes<F> {
188192

189193
// Rational from the parsed value, `sig * 2^exp`
190194
let parsed_rational = two_exp * sig.to_bigint().unwrap();
191-
let error = (parsed_rational - rational).abs();
195+
let error = (parsed_rational - &rational).abs();
192196

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

197-
// Our real value is allowed to be exactly at the midpoint between two values, or closer
198-
// to the real value.
199-
// todo: this currently allows rounding either up or down at the midpoint.
200-
// Is this correct?
201-
if &error <= half_ulp {
201+
// If we are within one error value (but not equal) then we rounded correctly.
202+
if &error < half_ulp {
202203
return Ok(());
203204
}
204205

206+
// For values where we are exactly between two representable values, meaning that the error
207+
// is exactly one half of the precision at that exponent, we need to round to an even
208+
// binary value (i.e. mantissa ends in 0).
209+
let incorrect_midpoint_rounding = if &error == half_ulp {
210+
if sig & F::SInt::ONE == F::SInt::ZERO {
211+
return Ok(());
212+
}
213+
214+
// We rounded to odd rather than even; failing based on midpoint rounding.
215+
true
216+
} else {
217+
// We are out of spec for some other reason.
218+
false
219+
};
220+
205221
let one_ulp = consts.half_ulp.get(&(exp + 1)).unwrap();
206222
assert_eq!(one_ulp, &(half_ulp * &consts.two), "ULP values are incorrect");
207223

@@ -210,6 +226,7 @@ impl<F: Float> FloatRes<F> {
210226
Err(CheckFailure::InvalidReal {
211227
error_float: relative_error.to_f64(),
212228
error_str: relative_error.to_string().into(),
229+
incorrect_midpoint_rounding,
213230
})
214231
}
215232

0 commit comments

Comments
 (0)