Stream: ideas

Topic: Upside down mode


view this post on Zulip Joe Ennis (Jan 29 2026 at 17:52):

So it's probably been around a year since I wrote any roc (been patiently observing new compiler progress), but I remember having a mild frustration when running into compile time type errors: I'd be writing some new functionality somewhere in the codebase, run, and see an error pop up relatively far away in an already working section. It totally makes sense with all of the type inference (the erroneous new code was being treated as the “truth”) but it turned addressing the problem into a little game of reverse engineering the error. It made me think - what if you could build/check with some kind of “upside down” flag? When the type checker would normally zig, it would zag. When it would normally go top down, it would go bottom up, and the error would suddenly be where you expect it.

I have no idea how hard this would be to implement, but it would be pretty neat.

view this post on Zulip Rick Hull (Jan 29 2026 at 19:32):

Stranger Things have happened, I suppose (couldn't resist)

view this post on Zulip Luke Boswell (Jan 29 2026 at 20:12):

In the new implementation all the error reports are gathered in an array and then rendered out later. Assuming you don't have so many errors you saturate that list, I don't see why you couldn't print them in reverse.

view this post on Zulip Luke Boswell (Jan 29 2026 at 20:12):

Maybe we could have a cli flag to reverse the errors even??

view this post on Zulip Norbert Hajagos (Jan 29 2026 at 22:25):

I don't think it's about the order of errors. Rather it's an ask to rethink how we report the same errors. Suppose you have a main function where you are doing stuff with a U8 called hp. Multiple fns have accepted hp as valid argument, so that "strengthens" the likelyness of you meaning x to be a U8. Then you write a fn named display_hp which just takes a healh param and returns an interploated string "HP: $(healt)". In main, you call display_hp(hp). This was a pretty common error with the rust compiler. It would tell you that display_hp expected a string (cause of no auto string conversion), but you've provided a number. I think the idea is to report instead that "hey, you are passing in a number from main, but you are tryingto interpolate that, which you can only do with strings".

I don't think a flag is the way to do this, but I see the apeal in the idea. I'd make a 2 way binding instead, where it's possible. Instead of saying 'this fn expects a string but you provided a number', we could say 'this fn expects a string because of this line: ... but you provided a number here: ...' . Ofc there's the question of "Would it result in err msgs that are too verbose with little benefits for programmers who've used the language for more than a couple of hours?"

view this post on Zulip Luke Boswell (Jan 29 2026 at 22:33):

I know @Jared Ramirez has been looking at the error reports lately. I think he's added some new systems to provide better hints (inspired from Elm). I'm sure he can explain what is possible here better than I. :smiley:

view this post on Zulip Jared Ramirez (Jan 29 2026 at 22:41):

Roughly what you described, Norbert, is how type checking already works. Whenever we see an unbound variable like x used like a u8, we infer that it's a u8. Then if you later attempt to use that as a string, you get a type error.

Often what compilers do, including ours and including Elm's, is only show the region where the error occurs for the second usage, meaning where you attempt to use it as a string. We print something like “you're using this type variable like a string but it's actually a u8”. And print the call site of where you try to use it as a string

From this point onward the variable with the error u8/string error becomes a new special internal type called ".err". This type can never fail to type check with anything else and propagates this “.err” type throughout all other usages of the variable. This mechanism is what prevents cascading type errors (meaning we already told you that you're using this variable like a U8 and string so we don't need to tell you all the other places that you use it where there might be mismatches)

view this post on Zulip Jared Ramirez (Jan 29 2026 at 22:44):

That said it's certainly possible to provide the region of the other usage (where you used it as a U8 originally) but yeah, like you said, error messages could be very verbose and there may be cases where that becomes confusing

view this post on Zulip Jared Ramirez (Jan 29 2026 at 22:46):

If you're modifying a top level definition without an annotation, then I could definitely see how tweaking something could cause errors to appear in far away parts of the codebase. This is because whenever there's an internal error to a function or a definition, but that definition has a type annotation, then that ".err” Propagation is stopped and all places that use the definition are treated like it had the type of the annotation. Basically the error is encapsulated inside the definition and it does not leak out.

If you're running into cases where something like this does not happen in the new compiler, then it's likely a bug!

view this post on Zulip Joe Ennis (Jan 29 2026 at 23:00):

In this example:

main! = |_args| {
    rec = create_record()
    use_record(rec)
}
create_record = || {
    {foo: "bar"}
}
use_record = |rec| {
    Stdout.line!(rec.blah)
}

you get the error:

The first argument being passed to this function has the wrong type:
  ┌─ main.roc:7:5
  │
7 │     use_record(rec)
  │                ^^^

This argument has the type:

    { foo: Str }

But use_record needs the first argument to be:

    { blah: Str, foo: Str }

but it would also make sense to see something like:

The return value from this function has the wrong type:
   ┌─ main.roc:10:5
   │
10 │     {foo: "bar"}
   │     ^^^^^^^^^^^^

The return value has the type:

    { foo: Str }

But needs to be be:

    { blah: Str }

It feels a little silly in this example, but with more complex errors and a greater distance between the two types that are mismatched, it may be helpful to have the option of going in reverse (if that makes any sense)

view this post on Zulip Luke Boswell (Jan 29 2026 at 23:00):

If you're modifying a top level definition without an annotation, then I could definitely see how tweaking something could cause errors to appear in far away parts of the codebase.

This makes me wonder if we could have a simple heuristic for error distance -- and if it's above some threshold we give a hint that adding type annotations on top-level declarations can help mitigate confusing bug reports and give clearer type errors.

Or maybe it would even be possible to sort the error reports on this kind of metric.

view this post on Zulip Joe Ennis (Jan 29 2026 at 23:03):

the mismatch creates a connection between the two areas of the code, but you may get the error somewhere far away from the mistake you just made (at the other end of the connection)

view this post on Zulip Joe Ennis (Jan 29 2026 at 23:07):

"oh I see it's this line that I just wrote that is busting everything up" instead of potentially... "huh"

view this post on Zulip Jared Ramirez (Jan 29 2026 at 23:13):

Joe Ennis said:

In this example:

main! = |_args| {
    rec = create_record()
    use_record(rec)
}
create_record = || {
    {foo: "bar"}
}
use_record = |rec| {
    Stdout.line!(rec.blah)
}

you get the error:

The first argument being passed to this function has the wrong type:
  ┌─ main.roc:7:5
  │
7 │     use_record(rec)
  │                ^^^

This argument has the type:

    { foo: Str }

But use_record needs the first argument to be:

    { blah: Str, foo: Str }

but it would also make sense to see something like:

The return value from this function has the wrong type:
   ┌─ main.roc:10:5
   │
10 │     {foo: "bar"}
   │     ^^^^^^^^^^^^

The return value has the type:

    { foo: Str }

But needs to be be:

    { blah: Str }

It feels a little silly in this example, but with more complex errors and a greater distance between the two types that are mismatched, it may be helpful to have the option of going in reverse (if that makes any sense)

I think the error message in the first case is actually incorrect because I don't think that use record should be requiring the argument to have the field foo. So I can look into that!

As to the second part, I actually don't agree that the second way makes sense. There's no issue with the create_record function or what it's returning. The issue is with what what use_record expects and what it’s being given.

Try to annotate all top level defs IMO, and the error messages will be better (but I know that this is just an example :grinning:)

view this post on Zulip Jared Ramirez (Jan 29 2026 at 23:15):

Luke Boswell said:

If you're modifying a top level definition without an annotation, then I could definitely see how tweaking something could cause errors to appear in far away parts of the codebase.

This makes me wonder if we could have a simple heuristic for error distance -- and if it's above some threshold we give a hint that adding type annotations on top-level declarations can help mitigate confusing bug reports and give clearer type errors.

Or maybe it would even be possible to sort the error reports on this kind of metric.

I know elm does this in some cases but I don't actually know the criteria that they use to decide to give that hint or not. I think they do for recursive types maybe?

view this post on Zulip Joe Ennis (Jan 29 2026 at 23:21):

Jared Ramirez said:

As to the second part, I actually don't agree that the second way makes sense. There's no issue with the create_record function or what it's returning. The issue is with what what use_record expects and what it’s being given.

It's tough with the contrived example, but having that second message instead (way back in the past when I ran into this) would have been super helpful. In my mental model, use_record is excellent, create_record is the whoops-a-daisy - backracking through a ton of lambdas and code back to the error may not be super obvious. Also even with annotations you can still run into the same problem, the annotations can just mismatch as well, and the confusion is still there.

In no way am I saying how it works now is wrong or inaccurate - it just in some instances isn't as helpful as it could be if ran in the upside down.


Last updated: Jun 16 2026 at 16:19 UTC