Skip to content

strconv: fix ParseFloat exponent and overflow bookkeeping - #129

Open
gaoflow wants to merge 2 commits into
tdewolff:masterfrom
gaoflow:fix-parsefloat-exponent-bookkeeping
Open

strconv: fix ParseFloat exponent and overflow bookkeeping#129
gaoflow wants to merge 2 commits into
tdewolff:masterfrom
gaoflow:fix-parsefloat-exponent-bookkeeping

Conversation

@gaoflow

@gaoflow gaoflow commented Jul 27, 2026

Copy link
Copy Markdown

ParseFloat([]byte("99999999999999999999.0")) returns 1e21 instead of 1e20, exactly a decade high: when the mantissa stops accumulating in the integer part, mantExp still subtracts one for a '.' that isn't between trunk and dot, so it counts dot-trunk+1 dropped digits instead of dot-trunk. Three more wrong answers share that bookkeeping — the overflow guard is missing the second clause ParseUint/ParseInt got in 2651d3a, so 18446744073709551616 wraps the accumulator and parses as 0; the int * 10^k path scales f and exp before deciding to bail out, so the fall-through applies the exponent twice and 993349238352373e23 comes back 10x high; and the final scaling multiplies by 10^-mantExp and 10^expExp separately, so either factor can leave math.Pow10's [-323,308] domain, collapsing a fully written-out 1e-289 to 0, turning 0.0…1e400 into +Inf and 0e309 into NaN.

Measured against strconv.ParseFloat over 381k generated literals: nothing the current code gets bit-exact changes, and 98k more become exact. The one-step f * math.Pow10(int(exp)) you'd expect here is deliberately not what I did — it fixes more but makes 11k currently-exact results 1 ulp worse, so the last step keeps the existing arithmetic and only re-scales when it degenerated to exactly 0 or ±Inf. The residual gap to the stdlib is the intended 19-20 digit mantissa truncation, a few ulp, so the added random differential asserts a relative bound rather than equality; go test ./... stays green, and reverting just the two source files fails 20 of the new cases plus 70313 of 200000 random literals.

This is reachable from minify's SVG path data (svg/pathdata.go), where <path d="M18446744073709551616 0L1 1"> currently minifies to m0 0L1 1, though it needs a longer literal than real path data usually carries. The second commit deletes a ParseDecimal branch whose 23 < exp && exp < 0 can never be true; writing it as intended (f / float64pow10[-exp], as float.go already does) moves 57344 of 400000 results closer to the stdlib but 11228 further, so I left that call to you.

gaoflow added 2 commits July 27, 2026 02:10
Four independent errors made ParseFloat return the wrong value:

- mantExp subtracted one for a '.' that does not lie between trunk and
  dot, so truncating inside the integer part dropped one digit too many
  and any such literal with a dot came out 10x too large.
- The uint64 overflow guard only checked MaxUint64/10 < n, missing the
  second clause ParseUint and ParseInt already have, so n wrapped and
  18446744073709551616 parsed as 0.
- The int * 10^k path scaled f and exp before deciding it could not
  return, so the fall-through applied the exponent twice.
- Scaling by 10^-mantExp and 10^expExp separately let either factor
  leave math.Pow10's domain, so in-range values underflowed to 0 or
  overflowed to +Inf, and a zero mantissa produced NaN.

Compared against strconv.ParseFloat over 381k generated literals no
result that was already bit-exact changes.
23 < exp && exp < 0 can never hold, so the negative-exponent fast path
never ran; indexing float64pow10 with a negative exp would panic.
Comment thread strconv/float.go
return f, i
}
h := f * math.Pow10(int(-mantExp))
h *= math.Pow10(int(expExp))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can both Pow10's be merged?

Comment thread strconv/float.go
}
h := f * math.Pow10(int(-mantExp))
h *= math.Pow10(int(expExp))
if h == 0.0 || math.IsInf(h, 0) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we check for 0.0 above with f==0.0?

Comment thread strconv/decimal.go
if 0 <= exp && exp < 23 {
return f * float64pow10[exp], i
} else if 23 < exp && exp < 0 {
return f / float64pow10[exp], i

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's fix this clause and do this like float, besides it moves more results closer according to your measurements.

@tdewolff

Copy link
Copy Markdown
Owner

This is fantastic work, thank you very much for the effort! The changes look good to me, just a few questions that surged are above. Your contribution is highly appreciated!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants