Stream: compiler development

Topic: float equality and static dispatch


view this post on Zulip Richard Feldman (Jul 03 2025 at 18:43):

I think we should revisit disallowing == on F32 and F64 (and therefore pattern matching on them too)

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:43):

main reason is that in the static dispatch world I'm not sure if it's feasible without sacrificing other goals

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:44):

in static dispatch, we need a Num.equals function and that's what a == b will desugar to

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:44):

when a and b have the type Num

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:44):

the obvious type for Num.equals is Num(a), Num(a) -> Bool

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:45):

if we need to make the type be "not a F32 and not a F64 but Dec is okay" then we need to make the number type system much more complicated than it is today

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:45):

which I don't think is a cost that's worth the benefit of disallowing equality for floats

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:46):

it would have to be strict bit equality right?

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:46):

I'm setting that aside for now

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:47):

it's more that we'd previously decided to disallow it, partly because it's error-prone and also because of what happens when you try to use infinity, -infinity, and NaN in dictionaries and sets

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:50):

so, one idea would be:

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:50):

yeah it's goofy

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:51):

yeah i kinda hate that last bullet point

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:52):

I'm open to suggested alternatives haha

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:52):

can we just have a hard coded prohibition on dicts and sets around prohibiting fp

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:52):

it's the same problem as Num.equals

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:52):

I think that's better than handing devs a footgun

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:52):

how do you represent it in the type system?

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:52):

Anthony Bullard said:

I think that's better than handing devs a footgun

that's why we originally disallowed this :smile:

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:53):

I'm not saying representing it at all.

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:53):

I definitely think it's a smaller footgun with crash than with it doing what happens by default with nan in dictionaries/sets

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:53):

Just a hard coded "i'm sorry dave..."

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:53):

at compile time?

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:53):

Yes with a clear explanation of why

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:54):

we could do a warning I guess

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:54):

NaNs are relatively easy to create especially through serialization/deserialization

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:54):

error seems weird, like it's a type mismatch but not a type mismatch :sweat_smile:

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:54):

Infinity not as much

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:55):

i don't think it would be a type mismatch,

view this post on Zulip Anthony Bullard (Jul 03 2025 at 18:55):

but at the very least a warning

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:57):

the counterargument (which I do think is reasonable) is that if someone's using floats over the default of Dec in general, it's because their use case needs hardware-accelerated performance more than it needs footgun avoidance, and so even knowing that floats are full of footguns they are making the choice

view this post on Zulip Richard Feldman (Jul 03 2025 at 18:57):

and so blocking them from using floats in these ways is just hurting them and not really helping them

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:01):

under that design philosophy, the purpose of the crash in dictionaries and sets would be the same as crash on integer overflow: if this ever happens, you have 100% for sure entered a broken state, and being loudly broken is better than being silently broken

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:02):

I agree, so maybe a warning is the appropriate thing

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:20):

well warning just means error that doesn't block you from running

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:20):

but warnings return nonzero exit code and they're intended to convey "you should 100% fix this at some point, but your program isn't necessarily going to crash just because you didn't fix it"

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 19:20):

If the assumption is floats are used presumably for performance, why not moving them out from the set of good friendly numbers that lack NaNs, Infinities and variable precision? Yes, it would break generics over usual numbers. But floats seem to be too specific

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:21):

because then you can't have like my_float + my_other_float

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:21):

that's not a realistic option :sweat_smile:

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:22):

a general theme with floats has been me trying to make them less error-prone and then gradually realizing that attempts to make them less error-prone also make them less usable for the thing they're actually good at, which is high-performance fractional math

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:23):

and at this point I'm leaning towards the actual answer being "use Dec if you want reliability, and if you want hardware-accelerated performance and footguns, use F32/F64 and be aware that here there be dragons"

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:23):

none of the experiments we've tried to make floats less error-prone have turned out well imo

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:24):

but offering a non-error-prone alternative has been great and is unaffected by what we do with floats

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 19:24):

because then you can't have like my_float + my_other_float.

Introduce a sister of Num, FloatNum and call it a day :smile:

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:24):

that doesn't work

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:24):

We could have Floating point operators

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:24):

numbers all need to have the base type Num and a different type parameter

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:25):

And then they can live on an island

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:25):

Anthony Bullard said:

We could have Floating point operators

yeah people love that about OCaml :joy:

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:25):

Like .+, .-, etc

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:25):

i'm just exploring the spaxe

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:25):

yeah I hear ya

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:25):

It's clearer what you are getting into

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:26):

There is rational behavior over here, and floats over there

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:26):

yeah, but like another thing I'm kind of tuned into here is that we're all coming at this from a web dev perspective

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:26):

in my language i tried to get around it by only having DEC64 as the number type

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:27):

I'm pretty sure if there were a Roc game dev in here they'd be like "wtf don't do that to me, I have to use floats all the damn time and I don't have a choice about it, why are you trying to make my life harder just bc I'm a game dev"

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:27):

But apparently a lot of people like to shit on DEC64 and i didn't have the time to dig into the specific claims

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:28):

GPU-powered native UI applications are in the same boat; F32 is what the GPU wants

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:28):

like probably if you're making a web app, more often than not just using Dec everywhere is the better choice, but Roc is aimed at a broader set of use cases

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:28):

Richard Feldman said:

I'm pretty sure if there were a Roc game dev in here they'd be like "wtf don't do that to me, I have to use floats all the damn time and I don't have a choice about it, why are you trying to make my life harder just bc I'm a game dev"

Roc is going to be high perf, but game dev level performant? i mean outside of Love2D style?

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 19:28):

game dev

yeah, I bet they love equality checks

view this post on Zulip Anthony Bullard (Jul 03 2025 at 19:28):

Richard Feldman said:

GPU-powered native UI applications are in the same boat; F32 is what the GPU wants

this is the real stickler

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:29):

Anthony Bullard said:

Richard Feldman said:

I'm pretty sure if there were a Roc game dev in here they'd be like "wtf don't do that to me, I have to use floats all the damn time and I don't have a choice about it, why are you trying to make my life harder just bc I'm a game dev"

Roc is going to be high perf, but game dev level performant? i mean outside of Love2D style?

people make a living on Unity games written in C#, and we should be faster than that

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 19:31):

Ok, floating equality is usually done with a fixed precision value?

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 19:33):

I mean, approximate equality, right? Also, warnings per equality check?

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:36):

in most languages with floats, equality compiles down to the CPU instructions for float equality, which has these semantics (as defined by the IEEE-754 hardware standard):

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 19:36):

And in the warnings you would see "it's an approximate equality with x tolerance. If you want bit eq, use this, if you want different tolerance so that"

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:36):

I shouldn't have said "warning" earlier - let's just say "compiler error"

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:37):

if we give a warning, or special compiler error, for using floats with equals, to me this is basically equivalent to changing how number types work without actually making the change, except much hackier

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:37):

I don't think we should do it

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:38):

I think a reasonable comparison is with integer addition

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:39):

we don't warn you at compile-time if you do a + b even though that can be a runtime crash if it overflows

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:39):

like we could say "you should consider using checked addition so you can handle overflow errors properly, or saturating addition if it's ok to round off to the nearest high number"

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:39):

and force you to do that everywhere

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:40):

but we who are not doing game dev or native GUI development in Roc know that this would be super annoying and make us not want to use the language, much more so than the errors it would prevent

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:40):

so we wouldn't support that change

view this post on Zulip Richard Feldman (Jul 03 2025 at 19:40):

and I don't think we'd support errors for floats either if we were forced by our use case to use them all over our code base

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 19:56):

I see. My take is that it's annoying searching for the place where two floats were compared if there was a bug. Especially in case of generic functions. But I lived like that and haven't died yet

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 20:08):

If we give a footgun, I think it makes sense to provide a pair of decent shoes. A linter? Does roc already have a debug assertion function?

view this post on Zulip Richard Feldman (Jul 03 2025 at 21:14):

expect is basically Rust's debug_assert!

view this post on Zulip Richard Feldman (Jul 03 2025 at 21:14):

I definitely would like to never have a linter in Roc

view this post on Zulip Richard Feldman (Jul 03 2025 at 21:15):

at least not the traditional kind that looks for generic "issues"

view this post on Zulip Richard Feldman (Jul 03 2025 at 21:15):

as opposed to project-specific ones where the end user sets rules like "hey we're trying to move away from Foo, so no new uses of it allowed!"

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 21:22):

Once the roc parser is in the wild, community driven linters will be a matter of time.
But speaking of linter, I mean more clippy than eslint

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 21:52):

I understand the sentiment that linters have slipped into style checkers. As a result you have purism in the name of democracy (yes, oxymoron) and part of the team go with "whatever" and the other hate unreasonable rule they regularly face that mostly hinders development.

But the roc linter can concentrate on heuristics of correctness and not style. So it might be a tool, not constitution.

view this post on Zulip Luke Boswell (Jul 03 2025 at 22:27):

Richard Feldman said:

because then you can't have like my_float + my_other_float

Can you explain this more? I thought with static dispatch as long as they had add : F32, F32 -> F32 then it would be ok to use +

view this post on Zulip Luke Boswell (Jul 03 2025 at 22:29):

Anthony Bullard said:

And then they can live on an island

Is this because they're floating... thanks Dad :wink:

view this post on Zulip Luke Boswell (Jul 03 2025 at 22:30):

Richard Feldman said:

and at this point I'm leaning towards the actual answer being "use Dec if you want reliability, and if you want hardware-accelerated performance and footguns, use F32/F64 and be aware that here there be dragons"

I think this is a very reasonable approach.

The "there be dragons" part is something we can also help with a lot more than in every other language where I assume these also exist.

view this post on Zulip Luke Boswell (Jul 03 2025 at 22:34):

I'm not sure about the "help a lot more than" -- because we're talking about runtime issues.

view this post on Zulip Kiryl Dziamura (Jul 03 2025 at 23:06):

I only hope that dragons won't be implicit

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:25):

Luke Boswell said:

Richard Feldman said:

because then you can't have like my_float + my_other_float

Can you explain this more? I thought with static dispatch as long as they had add : F32, F32 -> F32 then it would be ok to use +

oh you're right, sorry - that particular example would work, but you couldn't have (for example) the number literal 4.2 going from Frac(a) to F32 anymore (or else you couldn't have it for Dec either)

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:26):

number literals rely on all of the number literals having the same nominal type - Num - and then only the type variables change when you use them in more specific ways

view this post on Zulip Luke Boswell (Jul 04 2025 at 00:27):

The idea of pulling F32 and F64 out from Num is an option right? They're still in the stdlib and available for use if you need performance, but you know they're not the safe default options.

view this post on Zulip Luke Boswell (Jul 04 2025 at 00:29):

We could still have special handling support for them like 43.0f64

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:47):

we could, yes, and then Roc will never be a C# competitor for game dev :joy:

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:48):

or a JS competitor for cross-platorm native GUIs etc.

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:49):

I think a big part of the problem here is that when I was originally thinking about it, I was thinking about it as if people are going to use floats for performance in just like one hotspot in a large code base that really needs a little extra juice

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:49):

but I don't think that's right

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:50):

I think it's actually more like "my whole use case is floats, and if Roc's ergonomics around floats are terrible then I just will not use it for my use case"

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:50):

so I think a better mindset is like "make it really really ridiculously easy to not use floats if you don't have to, but if you do have to, make the experience as nice as possible"

view this post on Zulip Luke Boswell (Jul 04 2025 at 00:51):

Maybe this is a silly question.. but why are they more ergonomic in Num? Is it not casting between number types, like when adding an integer or similar?

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:51):

which I do think includes crashing when a NaN would get inserted into a set or dict, instead of making the set or dict start quietly behaving in ludicrous ways

view this post on Zulip Richard Feldman (Jul 04 2025 at 00:52):

so like if I want to write the number 4.2, just like that, and be able to use that exact syntax for an argument to a function that accepts a Dec and also a function that accepts a F32, then either they both need to be a Num nominal type (with different type parameters) or else we need a fancier type system just for numbers

view this post on Zulip Luke Boswell (Jul 04 2025 at 00:59):

Ah ofc. I forgot about the polymorphic type things.

view this post on Zulip Dan G Knutson (Jul 04 2025 at 00:59):

I'm only a newish, hobbyist game dev, but my impression is games people would generally also not want exact float equality. For example, the implementation of Vec3 equals in Unity has an epsilon range

view this post on Zulip Dan G Knutson (Jul 04 2025 at 01:02):

IIRC there was some prominent bug in Minecraft related to using float equality instead of a <= guard. Which is the kind of thing where the consequence is youtuber lore instead of someone losing money, but game devs get got by floats too

view this post on Zulip Richard Feldman (Jul 04 2025 at 01:12):

yeah the problem is I've heard it's like "sometimes you actually do want it and other times you want to avoid it"

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 01:26):

Richard Feldman said:

numbers all need to have the base type Num and a different type parameter

Given anything can implement plus and such in the new compiler, is this still strictly needed?

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 01:28):

Richard Feldman said:

GPU-powered native UI applications are in the same boat; F32 is what the GPU wants

Actually gpus really prefer bf16, tf32, or fp8. Fp32 is pretty outdated on gpus nowadays.

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 01:30):

Richard Feldman said:

which I do think includes crashing when a NaN would get inserted into a set or dict, instead of making the set or dict start quietly behaving in ludicrous ways

Honestly, if we are specially casing flaots anyway to do this, just ban hashing floats in general. I don't think there is a generic way in roc for a dictionary to check for this. So it has to be special cased.

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 01:32):

Richard Feldman said:

yeah the problem is I've heard it's like "sometimes you actually do want it and other times you want to avoid it"

I think it is 99% of the time you don't want it, but some hyper optimized code has uses.

view this post on Zulip Richard Feldman (Jul 04 2025 at 04:00):

banning floats from having hashing has the same problems as banning them from having equals

view this post on Zulip Richard Feldman (Jul 04 2025 at 04:12):

like basically if we want to have floats still be able to work with number literals but not support equals or hash, we have to change the Num hierarchy to something like this:

F32 : Num({
    size : Fraction(Binary32),
    has_nan : [Yes]
})

F32 : Dec({
    size : Fraction(Binary32),
    has_nan : [No]
})

U64 : Num({
    size : Integer(Unsigned64),
    has_nan : [No]
})

Num.equals :
    Num({ size, has_nan : [No] }),
    Num({ size, has_nan : [No] })
    -> Bool

Frac(size, has_nan) : Num({
    size : Fraction(size),
    has_nan,
})

Num.div :
    Frac(size, has_nan),
    Frac(size, has_nan)
    -> Frac(size, has_nan)

view this post on Zulip Richard Feldman (Jul 04 2025 at 04:14):

so the type complexity and learning curve for all numbers goes up, compile times get some amount worse because we have this extra stuff to check, and the payoff is that we have disallowed direct float equality in favor of having to do the (x >= y and x <= y) trick

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 04:37):

How would a dict crash when used with floats?

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 04:38):

I think it has to be some sort of special cases mechanism

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 04:38):

I think we should just special cases this

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 04:38):

Simply and am extra of condiction that if a hash resolves to a float create a compilation failure

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 04:38):

Like have it just happen after resolution

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 04:39):

That or have it crash at runtime (but compile time error is a better experience)

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 04:39):

Like I think should just not represent it in the type system at all. Totally a special case

view this post on Zulip Richard Feldman (Jul 04 2025 at 13:39):

this is interesting: https://internals.rust-lang.org/t/f32-f64-should-implement-hash/5436/4

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 14:04):

Yeah, reading some of that thread reinforces my opinion. We should just generate a custom compile time error if someone calls .hash on a float.

If a user wants float hashing, they should wrap the float in a custom type. The best part of this is that the special type also, can implement all the math functions and be nice enough to use.

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 14:08):

And I think this is an odd enough edge cases that it is worth making a special error for after type resolution. I don't think it is worth truly representing in the type system.

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 14:11):

Also, I guess if num was a more standard type instead of a magically dynamic type, this could be dealt with in pure roc by calling Num.hash, which would unwrap the outer type and then call Frac.hash, which would unwrap and call F64.hash, would either call crash or a new compileTimeError builtin. Frankly, you could just leave it as a header with no definition and that would technically work as a compile time error.

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 14:13):

But I don't think num has true unwrapping of the outer type. Instead, it magically works for different multiple types.

view this post on Zulip Richard Feldman (Jul 04 2025 at 16:27):

Brendan Hansknecht said:

Yeah, reading some of that thread reinforces my opinion. We should just generate a custom compile time error if someone calls .hash on a float.

I'm on mobile and don't have a quick explaination, but in general I don't think we should do this

view this post on Zulip Richard Feldman (Jul 04 2025 at 16:28):

this is basically saying "we should introduce the concept of secret type mismatches that aren't represented in the type system so you can no longer ever tell from looking at two types whether they're going to have a mismatch"

view this post on Zulip Richard Feldman (Jul 04 2025 at 16:29):

it means things like a package can publish a new version with identical type signatures, as a patch release, and now it breaks your builds because it secretly started using hashing somewhere deep behind the scenes and you were passing it a float

view this post on Zulip Brendan Hansknecht (Jul 04 2025 at 16:29):

Take a look at #ideas > Do we need Num anymore? I think we may be able to do this more properly.

view this post on Zulip Richard Feldman (Jul 04 2025 at 16:29):

if passing a particular type to a function can cause a type mismatch, that needs to be reflected in the type.


Last updated: Jul 06 2025 at 12:14 UTC