elm-format icon indicating copy to clipboard operation
elm-format copied to clipboard

Float constant is changed to incorrect value

Open malaire opened this issue 4 years ago • 12 comments

When I try to define a constant as

maxFloatAsFloat : Float
maxFloatAsFloat =
    18446744073709549568.0

elm-format changes that to

maxFloatAsFloat : Float
maxFloatAsFloat =
    18446744073709550000.0

This is incorrect. Value 18446744073709549568.0 can be represented exactly as a Float unlike 18446744073709550000.0.

p.s. This is largest possible Float value less than 2^64, i.e. 2^64 - 2048.

malaire avatar Jun 12 '20 16:06 malaire

An Elm/JS Float is a double precision floating point number https://en.wikipedia.org/wiki/Double-precision_floating-point_format Even though it uses 64 bits it doesn't have 64 bits worth of precision, trading off some of it for extra range (exponent). It has 52 bits of mantissa, which is 53 bits worth of precision (1 bit is implicit).

You can test your number in the JS console of your browser:

> 18446744073709549568.0
18446744073709550000

To show you what I mean about the limit of precision, you can test the following (2 ** 53) + 1 === 2 ** 53 which is true since the floating point format doesn't have enough precision to store that extra 1.

You can still represent numbers bigger than 2**53, just not all of them since you're hitting precision limits.

Hope this helps :smile:

basile-henry avatar Jun 24 '20 07:06 basile-henry

You don't understand the issue. I understand perfectly how floating point precision works.

Your example is incorrect because JS console doesn't show exact value but rounds it.

Here is correct example (using malaire/elm-uint64):

$ elm repl
> import UInt64
> UInt64.floor 18446744073709550000.0 |> UInt64.toString
"18446744073709549568" : String
> UInt64.floor 18446744073709549568.0 |> UInt64.toString
"18446744073709549568" : String

So while you are correct that not all values can be represented as Float, you are wrong about which value can be represented as Float. Number 18446744073709550000.0 can't be represented as Float but is rounded to 18446744073709549568.0, while 18446744073709549568.0 is not rounded as it can be represented exactly as Float.

malaire avatar Jun 24 '20 10:06 malaire

It looks like various languages disagree on how to do double to string conversions:

Rust:

fn main() {
    println!("{:?}", 18446744073709549568.0_f64);
    println!("{:?}", 18446744073709550000.0_f64);
}
➜ rustc test.rs && ./test
18446744073709550000.0
18446744073709550000.0

C:

#include <stdio.h>

int main() {
  printf("%lf\n", 18446744073709549568.0);
  printf("%lf\n", 18446744073709550000.0);

  return 0;
}
➜ gcc test.c && ./a.out
18446744073709549568.000000
18446744073709549568.000000

Haskell (how elm-format is implemented):

import Text.Printf

main = do
  printf "%f\n" (18446744073709549568.0 :: Double)
  printf "%f\n" (18446744073709550000.0 :: Double)
➜ runhaskell test.hs
18446744073709550000.0
18446744073709550000.0

It might be wrong but then your issue isn't really with elm-format as much as it is with these various languages and their standard libraries.

I would be interested to know why they're giving different results though :sweat_smile:

basile-henry avatar Jun 24 '20 10:06 basile-henry

It looks like various languages disagree on how to do double to string conversions:

It might be wrong but then your issue isn't really with elm-format as much as it is with these various languages and their standard libraries.

Since there is disagreement on how Float values like this should be represented, elm-format should leave Float literals as-is and not change them. So this is clearly elm-format issue.

malaire avatar Jun 24 '20 10:06 malaire

Looks like the rust conversion to uint64 does the same as you: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b1890ec671149101edbed17be38f3788

I agree that a formatter could treat literals as blackboxes (String) and print them as is, that would also solve other problems like this one: https://github.com/avh4/elm-format/issues/635

basile-henry avatar Jun 24 '20 10:06 basile-henry

A goal of elm-format is to format code canonically, such that it makes the actual behavior of the code more clear.

This example program: https://ellie-app.com/b6hgdKMgnFxa1

module Main exposing (main)

import Html

main =
    Html.text (Debug.toString 18446744073709549568.0)

displays the output "18446744073709550000"

So I think formatting it as

main =
    Html.text (Debug.toString 18446744073709550000.0)

is correct.

malaire's example with UInt64.floor also has the same behavior after being formatted, so I think the change is also correct in that case.

Are there other edge cases I'm missing? Can someone provide an example program where formatting it changes the behavior of the program?

avh4 avatar Sep 28 '20 05:09 avh4

A goal of elm-format is to format code canonically, such that it makes the actual behavior of the code more clear.

Exactly my point. Value 18446744073709549568.0 stays exactly same when put into Float while 18446744073709550000.0 does not stay same - it's converted to 18446744073709549568.0.

So 18446744073709549568.0 should be kept as-is and not changed to rounded inexact value.

Your example with Debug.toString is invalid because Debug.toString rounds the value and doesn't show it exactly.

Can someone provide an example program where formatting it changes the behavior of the program?

It doesn't change behavior - both 18446744073709549568.0 and 18446744073709550000.0 become same value as Float - i.e. 18446744073709549568.0.

malaire avatar Sep 28 '20 12:09 malaire

Can you show an example of some running code that demonstrates how the value is 18446744073709549568.0 and not 18446744073709550000.0 ? I haven't been able to come up with one yet.

avh4 avatar Sep 28 '20 18:09 avh4

Can you show an example of some running code that demonstrates how the value is 18446744073709549568.0 and not 18446744073709550000.0 ?

I already showed code which shows that 18446744073709550000.0 is "rounded" to 18446744073709549568.0:

$ elm repl
> import UInt64
> UInt64.floor 18446744073709550000.0 |> UInt64.toString
"18446744073709549568" : String

Here UInt64.floor converts the value it receives exactly to UInt64, which is then converted exactly to String.

UInt64.floor doesn't receive 18446744073709550000.0 as argument because that value can't be represented as Float, but it receives nearest value that can be represented as Float, i.e. 18446744073709549568.0.

When looking these values at base-2 it's clear that 18446744073709549568.0 is the actual "rounded" Float value and 18446744073709550000.0 is too exact to be represented as Float, which only has 53-bit mantissa:

18446744073709549568 = 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000 0000 18446744073709550000 = 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1001 1011 0000

malaire avatar Sep 28 '20 19:09 malaire

They seem to have the same bits in rust. Maybe I am missing something?

harrysarson avatar Sep 28 '20 19:09 harrysarson

Hmm, yeah, malaire, I guess that makes sense, that 18446744073709550000 can't be represented.

Do you happen to know why 0x 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1000 0000 0000 gets to-stringed as 18446744073709550000 both in browsers (and in Haskell)? They're rounding to 16 decimal digits for some reason?

avh4 avatar Sep 28 '20 20:09 avh4

@harrysarson Those bit values I showed above were 64-bit integer values, not Float values.

As I've said 18446744073709550000.0 can't be represented as Float but is always converted to 18446744073709549568.0 so your Rust example shows 18446744073709549568.0 as Float in both cases.

@avh4 Actually 17 digits (17th here happens to be zero) because any Float, when rounded to 17 digits, can be converted back to original value. It just seems to be a common custom to round the values as much as they can be rounded without changing behavior.

malaire avatar Sep 28 '20 20:09 malaire