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({})
}
Is it silently a decimal at comptime? Or using epsilon comparison? Neither are documented.
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?
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.
That's why it's strange that it is True in Roc.
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.
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
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
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).
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
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
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
As for Gleam, its Float type is strictly 64-bit (as it targets the Erlang BEAM VM or JavaScript).
I learned something today! :smile:
Thanks for looking into this!
I checked in Roc with F64, and it's indeed False. The comparison is only True with F32 numbers.
I thought both F32 and F64 have imprecision with these values, so I didn't consider that F32 vs. F64 would make a difference.
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.
Interesting question. I think you're right.
notramo has marked this topic as resolved.
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.
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
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