Skip to content

Commit 76b752c

Browse files
authored
strconv: fix handling of subnormal numbers like '1.23e-308'.f64() (fix #25751) (#25752)
1 parent bb53ff5 commit 76b752c

File tree

2 files changed

+49
-0
lines changed

2 files changed

+49
-0
lines changed

vlib/strconv/atof.c.v

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,30 @@ fn converter(mut pn PrepNumber) u64 {
298298
s0 = q0
299299
}
300300
}
301+
302+
// Handle subnormal (denormalized) numbers - very small numbers near zero
303+
//
304+
// Normal floats have an implicit leading 1 bit in their mantissa (like 1.xxxxx).
305+
// When numbers get too small (binexp < -1022), we can't represent them normally.
306+
// Instead, we use subnormals: set exponent to 0 and shift the mantissa right,
307+
// losing precision gradually. This prevents abrupt underflow to zero.
308+
//
309+
// Example: 1.23e-308 is smaller than the minimum normal float, so we:
310+
// 1. Keep the normalized mantissa from s2 and s1
311+
// 2. Shift it right to "denormalize" it (the leading 1 moves into the mantissa)
312+
// 3. Round correctly using the bits that were shifted out
313+
// 4. Return with exponent = 0 (subnormal marker)
314+
if binexp < -1022 && (s2 | s1) != 0 {
315+
shift := -1022 - binexp
316+
if shift > 60 {
317+
return if pn.negative { double_minus_zero } else { double_plus_zero }
318+
}
319+
shifted := ((u64(s2) << 32) | u64(s1)) >> u32(shift)
320+
q := (shifted >> 8) +
321+
u64((shifted >> 7) & 1 != 0 && ((shifted & 0x7F) != 0 || (shifted >> 8) & 1 != 0))
322+
return (q & 0x000FFFFFFFFFFFFF) | (u64(pn.negative) << 63)
323+
}
324+
301325
// rounding if needed
302326
/*
303327
* "round half to even" algorithm
@@ -374,6 +398,8 @@ fn converter(mut pn PrepNumber) u64 {
374398
result = double_plus_infinity
375399
}
376400
} else if binexp < 1 {
401+
// Should not reach here for subnormals anymore (handled earlier)
402+
// This is now only for true zeros
377403
if pn.negative {
378404
result = double_minus_zero
379405
} else {

vlib/strconv/atof_test.c.v

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,29 @@ fn test_atof() {
8282
println('DONE!')
8383
}
8484

85+
fn test_atof_subnormal() {
86+
// Test subnormal (denormalized) float numbers and edge cases
87+
// These are very small numbers close to the f64 minimum
88+
// IMPORTANT: Compare with hardcoded f64 literals, not .f64() which uses the same parser
89+
90+
// Normal numbers
91+
assert strconv.atof64('1.0e-250') or { panic('parse error') } == 1.0e-250
92+
assert strconv.atof64('2.5e-260') or { panic('parse error') } == 2.5e-260
93+
94+
// Transition zone
95+
assert strconv.atof64('1.0e-300') or { panic('parse error') } == 1.0e-300
96+
assert strconv.atof64('2.2250738585072014e-308') or { panic('parse error') } == 2.2250738585072014e-308
97+
98+
// Subnormal numbers (these fail without the fix)
99+
assert strconv.atof64('1.23e-308') or { panic('parse error') } == 1.23e-308
100+
assert strconv.atof64('1.0e-310') or { panic('parse error') } == 1.0e-310
101+
assert strconv.atof64('5.0e-320') or { panic('parse error') } == 5.0e-320
102+
assert strconv.atof64('5e-324') or { panic('parse error') } == 5e-324
103+
104+
// Negative subnormal
105+
assert strconv.atof64('-1.0e-320') or { panic('parse error') } == -1.0e-320
106+
}
107+
85108
fn test_atof_errors() {
86109
if x := strconv.atof64('') {
87110
eprintln('> x: ${x}')

0 commit comments

Comments
 (0)