When sprintf is called with a precision larger than the double's
significand width (e.g. "%.51g"), fixed_width() indexed pow10 tables
out of bounds and produced a negative shift exponent. Cap the
internal digit count to 18 in the %g branch, matching the existing
%e and %f branches; downstream loops already zero-pad to the
caller's precision so visible output is unchanged.
Co-authored-by: Claude <noreply@anthropic.com>
MinGW gcc emits -Wmaybe-uninitialized for `digs[0]` in the
`mrb_format_float` function because it cannot prove that
`count_digits()` always returns >= 1. The code is correct
(count_digits's `d == 0` early return makes the lower bound
clear to a human reader), but gcc's flow analysis does not
follow through. Initialize `digs` to silence the false positive
across all MinGW gcc CI configurations.
Co-authored-by: Claude <noreply@anthropic.com>
mrb_read_float jumped past the *fp assignment via `goto done` when
the exponent had no digits (e.g., "5e", "5e+"). It returned TRUE
without setting *fp, leaving the caller (mrb_str_to_dbl etc.) to
return whatever was on the stack. MSan flagged this; on most runs
the uninitialized read happens to yield 0.0, so the bug is silently
incorrect rather than crashing.
Refactor the finalization (compute res from d, final_p, sign, etc.)
to run once after the optional-exponent block. The malformed-exponent
case now falls through using the mantissa-only `final_p = trunc - dp`,
producing the same result strtod gives for the same input ("5e" -> 5.0
with endp at 'e'). Float("5e") still raises because mrb_str_len_to_dbl
rejects trailing characters under badcheck.
Reported by OSS-Fuzz (MSan).
Co-authored-by: Claude <noreply@anthropic.com>
Two UBSan issues exposed by sprintf("%f", 1e-7) and similar:
1. uscale() shifted hi by c.s without bounding c.s, hitting UB
when c.s >= 64. The mask line had `c.s & 63`, but the actual
`hi >> c.s` line did not, so the partial guard was incomplete.
On x86 the hardware silently masks the shift, producing wrong
output ("1844674407370.955078" for 1e-7) instead of crashing.
When c.s >= 64 the value rounds to 0 with sticky=1, so we can
bail early.
2. count_digits(0) called bits_len64(0) -> clz64(0), which is UB.
The only other bits_len64 caller already guards d == 0; align
count_digits with that pattern. Returning 1 (since "0" is one
digit) preserves output formatting.
Reported by OSS-Fuzz (clusterfuzz testcase 5210395240628224).
Co-authored-by: Claude <noreply@anthropic.com>
Use uscale-based shortest() to compute the minimal decimal string
that uniquely identifies each double. This guarantees perfect
round-trip (parse(to_s(x)) == x) while keeping output concise
(e.g. 0.1 prints as "0.1", not "0.10000000000000001").
Co-authored-by: Claude <noreply@anthropic.com>
Replace separate float formatting (fmt_fp.c) and parsing (readfloat.c)
implementations with a unified fp_uscale.c using 128-bit unrounded
scaling. Both mrb_format_float() and mrb_read_float() now share a
single pow10 table and uscale() primitive for decimal/binary conversion.
This fixes subnormal parsing accuracy (old code returned 0.0 for the
smallest subnormals) and corrects %.2f rounding for values like
12345.125. Table size grows from ~5KB to ~11KB in .rodata.
Co-authored-by: Claude <noreply@anthropic.com>