Stream: beginners

Topic: ✔ Why is `0.1 + 0.2 == 0.3` actually `True` in Roc?


view this post on Zulip notramo (Oct 07 2026 at 18:08):

IEEE 754 floats should not be equal in the following snippet.

main! = |_| {
    echo!(
        if (0.1.F32 + 0.2.F32 == 0.3.F32)
            "true\n"
        else
            "false\n",
    )

    Ok({})
}

view this post on Zulip notramo (Oct 07 2026 at 18:09):

Is it silently a decimal at comptime? Or using epsilon comparison? Neither are documented.

view this post on Zulip Aurélien Geron (Oct 07 2026 at 18:21):

There's no way to represent 0.3 perfectly using a F32, so perhaps both 0.1+0.2 and 0.3 end up with the same representation? And even if they don't, this comparison may not run a bit-wise equality check, I think it runs the standard F32 equality test, which might be more tolerant?

view this post on Zulip notramo (Oct 07 2026 at 19:22):

There's no way to represent 0.3 perfectly using a F32, so perhaps both 0.1+0.2 and 0.3 end up with the same representation?

No. This equality comparison is a common example that demonstrates the inaccuracies of IEEE 754 floats. It should be false. It's false in Zig, JavaScript, Crystal, and Gleam.

view this post on Zulip notramo (Oct 07 2026 at 19:23):

That's why it's strange that it is True in Roc.

view this post on Zulip Aurélien Geron (Oct 07 2026 at 20:17):

Interesting. I just ran this code in the REPL:

» (0.1.F32 + 0.2.F32).to_bits()
1050253722
» (0.3.F32).to_bits()
1050253722

Same representation, apparently. So if there's a bug, it might be in F32.plus rather than F32.is_eq (or F32.is_float_eq). Edit: tests below show that Roc is behaving just like C, Zig, JS, etc.

view this post on Zulip Aurélien Geron (Oct 07 2026 at 20:19):

For F64, we get this result:

» (0.1.F64 + 0.2.F64).to_bits()
4599075939470750516
» (0.3.F64).to_bits()
4599075939470750515
» 0.1.F64 + 0.2.F64 == 0.3.F64
False

view this post on Zulip Aurélien Geron (Oct 07 2026 at 20:22):

Note that C agrees with Roc:

#include <stdio.h>

int main() {
    printf("F32: 0.1f+0.2f==0.3f -> %d, sum=%.17g\n", 0.1f+0.2f == 0.3f, 0.1f+0.2f);
    printf("F64: 0.1+0.2==0.3    -> %d, sum=%.17g\n", 0.1+0.2 == 0.3, 0.1+0.2);
    return 0;
}

Output:

F32: 0.1f+0.2f==0.3f -> 1, sum=0.30000001192092896
F64: 0.1+0.2==0.3    -> 0, sum=0.30000000000000004

view this post on Zulip Aurélien Geron (Oct 07 2026 at 20:24):

The approximations happen to line up perfectly in F32, so 0.1 + 0.2 evaluates to exactly the same bit pattern as 0.3.

It's kind of weird that other languages would return a different result. At least in the case of Javascript, I'm pretty sure that floats are 64-bit, so that might be why (edit: it's 64-bit by default but it does provide ways to force a 32-bit representation, see below). Not sure about zig, crystal and gleam, though (edit: Zig and Crystal agree with Roc, and Gleam only supports 64-bit floats, see below).

view this post on Zulip Aurélien Geron (Oct 07 2026 at 20:26):

Actually, I just tested in Zig, it also agrees with Roc:

const std = @import("std");

pub fn main() void {
    const a32: f32 = 0.1; const b32: f32 = 0.2; const c32: f32 = 0.3;
    const a64: f64 = 0.1; const b64: f64 = 0.2; const c64: f64 = 0.3;

    std.debug.print("F32: 0.1+0.2==0.3 -> {}, sum={d:.17}\n", .{a32 + b32 == c32, a32 + b32});
    std.debug.print("F64: 0.1+0.2==0.3 -> {}, sum={d:.17}\n", .{a64 + b64 == c64, a64 + b64});
}

Output:

F32: 0.1+0.2==0.3 -> true, sum=0.30000000000000000
F64: 0.1+0.2==0.3 -> false, sum=0.30000000000000004

view this post on Zulip Aurélien Geron (Oct 07 2026 at 20:28):

Yeah, Javascript also agrees with Roc:

// F32 using Math.fround() to force 32-bit precision
const a32 = Math.fround(0.1), b32 = Math.fround(0.2), c32 = Math.fround(0.3);
const sum32 = Math.fround(a32 + b32);
console.log(`F32: 0.1+0.2==0.3 -> ${sum32 === c32}, sum=${sum32}`);

// F64 using standard JS Numbers
const a64 = 0.1, b64 = 0.2, c64 = 0.3;
const sum64 = a64 + b64;
console.log(`F64: 0.1+0.2==0.3 -> ${sum64 === c64}, sum=${sum64}`);

Output:

F32: 0.1+0.2==0.3 -> true, sum=0.30000001192092896
F64: 0.1+0.2==0.3 -> false, sum=0.30000000000000004

view this post on Zulip Aurélien Geron (Oct 07 2026 at 20:32):

Crystal agrees with Roc:

sum32 = 0.1_f32 + 0.2_f32
printf("F32: 0.1f+0.2f==0.3f -> %d, sum=%.17g\n", sum32 == 0.3_f32 ? 1 : 0, sum32)

sum64 = 0.1_f64 + 0.2_f64
printf("F64: 0.1+0.2==0.3    -> %d, sum=%.17g\n", sum64 == 0.3_f64 ? 1 : 0, sum64)

Output:

F32: 0.1f+0.2f==0.3f -> 1, sum=0.30000001192092896
F64: 0.1+0.2==0.3    -> 0, sum=0.30000000000000004

view this post on Zulip Aurélien Geron (Oct 07 2026 at 20:34):

As for Gleam, its Float type is strictly 64-bit (as it targets the Erlang BEAM VM or JavaScript).

view this post on Zulip Aurélien Geron (Oct 07 2026 at 20:37):

I learned something today! :smile:

view this post on Zulip notramo (Oct 07 2026 at 20:57):

Thanks for looking into this!

view this post on Zulip notramo (Oct 07 2026 at 20:58):

I checked in Roc with F64, and it's indeed False. The comparison is only True with F32 numbers.

view this post on Zulip notramo (Oct 07 2026 at 21:01):

I thought both F32 and F64 have imprecision with these values, so I didn't consider that F32 vs. F64 would make a difference.

view this post on Zulip Steve Howell (Oct 08 2026 at 01:33):

For any math folks out there, I'm kinda curious if F64 closes the door on this. I suspect there are infinite widths where they're equal and infinite widths where they're off by a bit. But I truly don't know.

view this post on Zulip Aurélien Geron (Oct 08 2026 at 02:47):

Interesting question. I think you're right.

view this post on Zulip Notification Bot (Oct 08 2026 at 08:50):

notramo has marked this topic as resolved.

view this post on Zulip Aurélien Geron (Oct 08 2026 at 09:05):

Steve Howell said:

For any math folks out there, I'm kinda curious if F64 closes the door on this. I suspect there are infinite widths where they're equal and infinite widths where they're off by a bit. But I truly don't know.

You successfully nerd-sniped me, I just had to research this. :sweat_smile: My conclusion is that 0.1+0.2 == 0.3 if and only if the number of precision bits is an even number: F32 has 24 precision bits, which is even, so 0.1+0.2 == 0.3. But F64 has 53 bits of precision, which is odd, so 0.1+0.2 != 0.3. Indeed, 0.1, 0.2, and 0.3, all end with repeating 0011 in binary notation. Depending on where you truncate this sequence (rounding the result, with ties go toward even), we get perfect equality or an off-by-one in the last bit.

view this post on Zulip Trevor Settles (Oct 08 2026 at 17:33):

Roc also has a Dec type. I wasn't quite sure how to ask the repl if this is the type its using, but Dev should be able to represent exact decimal values

view this post on Zulip Steve Howell (Oct 08 2026 at 19:49):

Aurélien Geron said:

Steve Howell said:

For any math folks out there, I'm kinda curious if F64 closes the door on this. I suspect there are infinite widths where they're equal and infinite widths where they're off by a bit. But I truly don't know.

You successfully nerd-sniped me

Thank you for looking into this! I wasn't thinking that F64 had 53 bits of precision. I would guess that future architectures like F256 (if there's actually a reason to go that high) would generally have even bits of precision.


Last updated: Oct 15 2026 at 17:56 UTC