Stream: compiler development

Topic: operator desugaring type symmetry


view this post on Zulip Richard Feldman (Nov 25 2025 at 02:16):

FYI, I discovered that we need to make binops have a type constraint where both sides are equal after all (I previously thought we could get away without doing this, but turns out not!)

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:16):

example situation where the unconstrained design (where e.g. a * b desugars to a.times(b) and that's it, a and b are permitted to have totally different types) breaks down:

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:17):

answer : Answer
answer = {
    my_custom_number : MyVector
    my_custom_number = 42

    5.times(my_custom_number)
}

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:17):

(that's after desugaring 5 * my_custom_number to 5.times(my_custom_number))

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:17):

the type of that 5 literal is:

a where [
    a.from_numeral : a -> Try(a, InvalidNumeral(Str)),
    a.times : a, MyVector -> Answer,
]

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:18):

since nothing is forcing this to be concrete, this ends up "defaulting to Dec"

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:19):

however, the way that works is that we unify Dec with this type - which fails, because Dec.times : Dec, Dec -> Dec fails to unify with a.times : a, MyVector -> Answer even if Answer turns out to be a type alias for Dec, because Dec will fail to unify with MyVector

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:19):

so we know it can't be Dec, but we can't leave it as unbound, so...what do we do?

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:19):

one design is that we give an error, which means Roc is a language where you can't write 2 * x or 1 + x etc. - which would be absurd; obviously we're not going with that design

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:20):

another design is where we do our best to guess what number type this should be, since it can't be Dec but we also can't leave it as an unbound variable because we have no how to do operations on that

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:21):

so how would we guess? the only clue we have to go on in that entire type is MyVector, so even if we hand-wave away what the exact heuristic is, it would have to conclude that MyVector is the type to go with

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:25):

at which point there's only one thing for it to do: unify MyVector with this type, which in turn means it has to unify MyVector.times : MyVector, _ -> _ with a.times : a, MyVector -> Answer - a type where the only way it can _possibly_ successfully unify is if MyVector.times has both arguments being the same type

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:26):

at which point the final design option, which I conclude is the correct one by process of elimination, is just a strictly clearer and more performant (in terms of compiler performance, due to not needing to have a heuristic for howto guess this) design compared to the second option, is:

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:26):

we require that both sides of the binop have the same type

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:27):

so it's not _just_ a * b is sugar for a.times(b), it's that it's a * b is sugar for "a.times(b) where a and b have the same type"

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:29):

at that point this is all trivial because they just unify

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:36):

so none of these problems exist

view this post on Zulip Richard Feldman (Nov 25 2025 at 02:37):

because 5.times(blah) just unifies to both arguments of times being the same, which means we just end up dispatching on whatever the type of blah is and we're all set

view this post on Zulip Andres Villegas (Nov 28 2025 at 07:11):

I think one of a common patterns in scientific computing which as far as I know is a motivation to have "operator overloading" is to enable stuff like "5 * x" where x is not necessarily another number, but can be a vector or a matrix or a tensor.

view this post on Zulip Andres Villegas (Nov 28 2025 at 07:12):

Will this design limit those use cases?

view this post on Zulip Fabian Schmalzried (Nov 28 2025 at 12:06):

I think you could implement a from_numeralto turn the number into the same type as the matrix. Might be a bit of effort so that both 5 + x and 5 * x all do the right thing, but I think it should be possible?


Last updated: Nov 28 2025 at 12:16 UTC