roc-lang / roc

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

Compiler Panic "constructor must be known in the indexable type if we are exhautiveness checking" #6594

Open jeffnm opened 6 months ago

jeffnm commented 6 months ago

I've found a compiler panic with roc_nightly-linux_x86_64-2024-03-16-49862da

thread '<unnamed>' panicked at 'constructor must be known in the indexable type if we are exhautiveness checking', crates/compiler/can/src/exhaustive.rs:211:41
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The following code reproduces the issue, along with two comments with changes that fix the panic and get back to normal compiler errors for bad code like this.

My understanding of what's happening is that the second pattern match when command is has a pattern that can't exist in the definition of command and there are a bunch of patterns in the definition of command that aren't covered in the pattern matching. The combination of both issues is what is breaking the compiler. Fixing either issue (via the commented changes) allows the compiler to give useful feedback again.

I'd hope this isn't a normal situation. I got into this situation by copying and pasting code and only modifying part of it. A more helpful error message here would certainly help newcomers who make the same mistake.

app "panic"
    packages {
        cli: "https://github.com/roc-lang/basic-cli/releases/download/0.8.1/x8URkvfyi9I0QhmVG98roKBUs_AZRkLFwFJVJ3942YA.tar.br",
    }
    imports [
        cli.Stderr,
        cli.Task.{ Task, await },
    ]
    provides [main] to cli

main : Task {} I32 
main = runTask |> Task.onErr handleErr

handleErr : _ -> Task {} I32
handleErr = \err ->

    errorMessage = Inspect.toStr err

    {} <- Stderr.line "$(errorMessage)" |> Task.await

    Task.err 1

runTask : Task {} _
runTask =

    command =
        when (input, model.state) is
            (KeyPress Up, _) -> MoveCursor Up
            (KeyPress Down, _) -> MoveCursor Down
            (KeyPress Left, _) -> MoveCursor Left
            (KeyPress Right, _) -> MoveCursor Right
            (KeyPress Enter, HomePage) -> UserToggledScreen
            (KeyPress Enter, ConfirmPage s) -> UserWantToDoSomthing s
            (KeyPress Escape, ConfirmPage _) -> UserToggledScreen
            # Fix one: comment out next line and uncomment the following one
            (KeyPress Escape, _) -> Exit
            # (KeyPress Escape, _) -> Quit
            (KeyPress _, _) -> Nothing
            (Unsupported _, _) -> Nothing
            (CtrlC, _) -> Exit
            (CtrlS,_) | (CtrlZ,_) | (CtrlY,_) -> Nothing

    modelWithInput = { model & inputs: List.append model.inputs input }

    # Action command
    when command is
        Nothing -> Task.ok (Step modelWithInput, NoOp)
        Quit -> Task.ok (Done { modelWithInput & state: UserExited }, NoOp)
        MoveCursor direction -> Task.ok (Step (Core.updateCursor modelWithInput direction), NoOp)
        # Fix two: uncomment next line
        # _ -> Task.ok (Step {modelWithInput}, NoOp)

Let me know if you need any more information.

smores56 commented 5 months ago

DISREGARD FOR BETTER ANALYSIS IN BELOW COMMENT

I also ran into this issue while doing library dev in Roc quite a few times. This is the simplest I can get the above example down to:

app "panic"
    packages { 
        cli: "https://github.com/roc-lang/basic-cli/releases/download/0.9.0/oKWkaruh2zXxin_xfsYsCJobH1tO8_JvNkFzDwwzNUQ.tar.br"
    }
    imports [
        cli.Stdout,
        cli.Task.{ Task },
    ]
    provides [main] to cli

# This type annotation doesn't change whether the compiler panics
main : Task {} I32
main =
    when nextColor Red is
        Red -> Stdout.line "The next color is red."
        # This also fixes the issue by making Blue a valid variant
        # Blue -> Stdout.line "The next color is blue."

# This type annotation fixes the issue, in that the compiler knows what's wrong
# nextColor : [Red] -> [Red]
nextColor = \color ->
    when color is
        Red -> Blue

It seems that when we don't tell the compiler that Red is the only option that nextColor can return via the type annotation, then it assumes that Blue must be a valid output variant. This constraint is imposed in the "Closed Union" matching in main, and adding a blue arm to the when clause also fixes the issue.

An easy patch would be to throw an actual compilation error that links this GitHub issue instead of panicking, but then the issue remains.

Better would be to somehow reconcile unannotated (e.g. inferred) function return types with exhaustive when matches. My expected user experience would assume correctness at nextColor (being an atomic code unit), whose inferred type is [Red] -> [Blue] and say that the matched [Red] variant doesn't match nextColors [Blue]. I expect that may collide with the expected Roc user experience, so I'd like to hear the team's opinion before we move forward with fixing this.

If the above proposed solution is good for the team, then I nominate myself to try fixing this issue. I don't have experience making changes to the Roc compiler, so if you'd rather leave this to a more experienced contributor, I take no offense.

DISREGARD FOR BETTER ANALYSIS IN BELOW COMMENT

smores56 commented 5 months ago

I also ran into this issue while doing library dev in Roc quite a few times. This is the simplest I can get the above example down to:

app "panic"
    packages { 
        cli: "https://github.com/roc-lang/basic-cli/releases/download/0.9.0/oKWkaruh2zXxin_xfsYsCJobH1tO8_JvNkFzDwwzNUQ.tar.br"
    }
    imports [
        cli.Stdout,
        cli.Task.{ Task },
    ]
    provides [main] to cli

# This type annotation doesn't change whether the compiler panics
main : Task {} I32
main =
    # This fixes the issue by (I think) constraining what the `when` expects
    # color : [Blue]
    color = Blue

    when color is
        Red -> Stdout.line "The color is red."
        # This fixes the issue by making Blue a valid variant,
        # though we still get a compiler error since `Red` isn't expected
        # Blue -> Stdout.line "The next color is blue."

It seems that when we don't tell the compiler what variants color can be, it gets confused between the constraints that: 1) color is inferred to at least be [Blue]a. 2) The when exhaustively matches only [Red].

While finding Red's constructor (during reification?), it assumes it will be present, but only sees [Blue], so it panics. The two possible fixes outline the two possible compiler errors we could add here:

If the second proposed solution is good for the team, then I nominate myself to try fixing this issue. I don't have experience making changes to the Roc compiler, so if you'd rather leave this to a more experienced contributor, I take no offense.

ayazhafiz commented 5 months ago

I think the second solution is ideal. The fix for this particular instance should be pretty straightforward; in particular, I think it should be sufficient to remove the assertion

https://github.com/roc-lang/roc/blob/c1d0c24194764fabb49e53dc8cfd03b13f74fe00/crates/compiler/can/src/exhaustive.rs#L223

and gracefully handle the missing constructor.

In general I believe the modeling of this could be better (#4440), but that is probably a story for another day. If it would be helpful any time during your implementation, feel free to message on Zulip!

Thanks for the great analysis and volunteering!

ageron commented 2 days ago

I got the same bug with the following code:

app [main] {
    pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br",
}

import pf.Stdout

table = Dict.fromList [
    ("AUG", Methionine),
    ("UAA", Stop),
    ("UUC", Phenylalanine),
]

translate = \codons ->
    codons
    |> List.walkUntil [] \protein, codon ->
        when table |> Dict.get codon is
            Ok Stop -> Break protein
            Ok aminoAcid -> Continue (protein |> List.append aminoAcid)
            Err NotFound -> Break []

main =
    translate ["AUG", "AUG", "UUC", "UAA", "UUC", "AUG"]
        |> Inspect.toStr
        |> Stdout.line!

The error message is:

thread '<unnamed>' panicked at crates/compiler/can/src/exhaustive.rs:211:41:
constructor must be known in the indexable type if we are exhautiveness checking
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

If you replace NotFound with KeyNotFound in my code, then everything works fine. But it took me an hour to figure it out: instead of a panic, there should have been a clear error message.