Summary
Serializing a Value that holds an f32 and parsing it back does not preserve
the number: the type flips f32 → f64, and for some values the numeric value
drifts too. It affects essentially every fractional f32 value under the
default (compact) config.
This is Value-only — typed f32/f64 fields are unaffected (the type is
known, so parsing is exact). It also does not occur for f32 values whose
shortest decimal is itself exactly f32-representable (e.g. 0.5, small
integers).
Repro
use ron::value::{Number, Value};
// Type is lost: F32 -> F64
let v = Value::Number(Number::F32(0.1_f32.into()));
let s = ron::to_string(&v).unwrap(); // "0.1"
let back: Value = ron::from_str(&s).unwrap();
assert_eq!(v, back); // FAILS: F32(0.1) != F64(0.1)
// Value drifts, not just the type:
let v = ron::from_str::<Value>("924444480.0").unwrap(); // F32(924444480)
let s = ron::to_string(&v).unwrap(); // "924444500.0"
let back: Value = ron::from_str(&s).unwrap(); // F64(924444500.0)
assert_eq!(v, back); // FAILS: value drifted 924444480 -> 924444500
Root cause
An asymmetry between the two sides:
- Serializer (
src/ser/mod.rs serialize_f32): write!(output, "{}", v) emits f32's shortest decimal — the shortest string that round-trips when re-parsed as f32.
- Parser (
src/parse.rs, the ParsedFloat discriminator): a suffix-less float literal is parsed as f64, then narrowed with if value.total_cmp(&f64::from(value as f32)).is_eq() → F32 else F64.
So the serializer's shortest-f32 decimal is fed to a parser that reads it as
f64 first. 0.1f32 prints as "0.1", which as f64 is 0.1f64 and does not
round-trip through f32, so it lands as F64. 924444480f32 prints as
"924444500.0" (a different decimal that maps to the same f32), which as
f64 is 924444500 → F64(924444500).
Mitigation / possible fixes
number_suffixes already disambiguates this — to_string_pretty(.., PrettyConfig::default().number_suffixes(true))
emits 0.1f32, which round-trips exactly. Two things make it not quite a full
answer today:
- it is off by default, so
Value round-trips are lossy out of the box;
- compact
to_string ignores it entirely (number_suffixes() only reads a
pretty config), so a lossless compact f32 Value round-trip isn't
reachable via the public API.
Not sure what the intended contract is here — filing in case Value round-trip
fidelity for f32 is meant to hold. Reproducing tests (both the drift and the
number_suffixes mitigation) are in #612 (tests/zz_finding_float_precision.rs),
part of a broader external robustness-test PR.
Summary
Serializing a
Valuethat holds anf32and parsing it back does not preservethe number: the type flips
f32 → f64, and for some values the numeric valuedrifts too. It affects essentially every fractional
f32value under thedefault (compact) config.
This is
Value-only — typedf32/f64fields are unaffected (the type isknown, so parsing is exact). It also does not occur for
f32values whoseshortest decimal is itself exactly
f32-representable (e.g.0.5, smallintegers).
Repro
Root cause
An asymmetry between the two sides:
src/ser/mod.rsserialize_f32):write!(output, "{}", v)emitsf32's shortest decimal — the shortest string that round-trips when re-parsed asf32.src/parse.rs, theParsedFloatdiscriminator): a suffix-less float literal is parsed asf64, then narrowed withif value.total_cmp(&f64::from(value as f32)).is_eq()→F32elseF64.So the serializer's shortest-
f32decimal is fed to a parser that reads it asf64first.0.1f32prints as"0.1", which asf64is0.1f64and does notround-trip through
f32, so it lands asF64.924444480f32prints as"924444500.0"(a different decimal that maps to the samef32), which asf64is924444500→F64(924444500).Mitigation / possible fixes
number_suffixesalready disambiguates this —to_string_pretty(.., PrettyConfig::default().number_suffixes(true))emits
0.1f32, which round-trips exactly. Two things make it not quite a fullanswer today:
Valueround-trips are lossy out of the box;to_stringignores it entirely (number_suffixes()only reads apretty config), so a lossless compact
f32Valueround-trip isn'treachable via the public API.
Not sure what the intended contract is here — filing in case
Valueround-tripfidelity for
f32is meant to hold. Reproducing tests (both the drift and thenumber_suffixesmitigation) are in #612 (tests/zz_finding_float_precision.rs),part of a broader external robustness-test PR.