roc-lang / roc

A fast, friendly, functional language.
https://roc-lang.org
Universal Permissive License v1.0
4.44k stars 312 forks source link

Monomorphization panics on invalid `Num` types #1293

Open rtfeldman opened 3 years ago

rtfeldman commented 3 years ago

If I write this, everything is fine:

test : Num (Integer Signed16)
test = 5

However, if I write this...

test : Num (Integer {})
test = 5

...monomorphization panics:

Unrecognized Num type argument for var 73 with Content: Structure(Record({}, 1))', compiler/mono/src/ir.rs:7228:13
rtfeldman commented 3 years ago

My first thought for how to make this nicer: introduce a new constraint in type checking along the lines of "is a valid number" and give a helpful type error if it doesn't unify to a known-valid Num type.

However, the more I think about this, the more complicated it seems like it would be. For example, here's an equivalent one done with type aliases:

X : {}
Z a : Num a

test : Z X
test = 5

How would we propagate the "must be number-friendly" constraint? We'd presumably have to do something like what Elm does with number, except behind the scenes instead of having it be reflected in the type variable name.

Hidden constraints like that can create other edge cases; e.g. if I have a package which publishes a Foo a type, and I change it so that a ends up needing to be number-friendly, that would need to be a breaking change to the package even though the types didn't change, because it could break someone's build.

rtfeldman commented 3 years ago

Alternative ideas:

  1. Don't give an error, but silently treat all unrecognized Num arguments as Num * - that is, I64 by default. Similarly with Float and Float *. That's not great; if a mistake was made, we should report it!
  2. Give an error during monomorphization. That's also not great, because you won't see it in the editor when editing normally (since the editor won't run monomorphization until you actually try to run your program). It might be a "good enough for now" solution though, since it'll come up almost never in practice.
rtfeldman commented 3 years ago

Another idea: all unrecognized Num arguments are the equivalent of U0 - that is, a number that's always zero at runtime, which is to say, effectively as useful as {}. The only value you can give to satisfy any of these is0, so if you did this...

test : Num {}
test = 5

...it would be handled the same way as any number literal that's too big for its type, like trying to assign 300 to a U8.

This would also be a separately useful language feature, because it would allow for "typed zeroes" which work like Rust's PhantomData type - they're a parameterized type which is always zero-sized (and thus optimized away at compile time, never adding runtime overhead). Right now Roc doesn't have a way to do that!

Incidentally, all the usual Num operations would work as normal, e.g. addition:

zero : Num [ Foo, Blah Str ]
zero = 0

stillZero : Num [ Foo, Blah Str ]
stillZero = zero + zero

...although behind the scenes, since these would be zero-sized types, we'd just monomorphize Num.add on unrecognized Num types to a function that ignores its arguments and always returns 0. Same for other such numeric functions.