Compiler Panic "constructor must be known in the indexable type if we are exhautiveness checking"
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.
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
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:
coloris inferred to at least be[Blue]a.- The
whenexhaustively 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:
- Adding the type annotation to
colorlets the compiler know that thewhen's[Red]mismatches withcolor's[Blue]. If we wanted to throw the compiler error here, we'd need to prioritize thewhenclause's exhaustive variants, and if we can't find a variant, we throw an error at the source of the issue, a.k.a. the definition ofcolorin this example. This moves the error away from what seems to be the real location, so I prefer the second compiler error. - Adding another arm to the
whenclause with either aBluecase or a catch-all (e.g._other ->) means thewhenis matching over an open union[Red]*, and won't fail when it sees blue. I suggest that when we fail to find a constructor for awhenenum variant, we show an error saying:- "The
whenclause here exhaustively checks the type[Red], but the value it matches over is at least[Blue]. Consider adding either cases for the missing variants or a catch-all arm_ -> ...to handle the other variants." - This should be easier to implement and give developers better colocation of the issue and the compiler error
- "The
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.
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!
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.