Error codes
The Haskell Foundation has accepted a proposal I submitted about improving the UX around error messages from Haskell tooling. Last week, @david-christiansen and I hatched a plan for creating a website where GHC users can see information about GHC error messages (and warnings), seeing examples and fixes, and learning more about the theory behind the error message (if applicable). David is overseeing the creation of the website itself, and I'm in charge of the GHC piece. We hope to expand this beyond GHC in due course, but GHC is enough of a target to start with.
The first step: assign unique error codes to every GHC error message. (This is essentially well-received-but-abandoned proposal #325.) Previously unimaginable, this is now within reach, given the structured error types GHC now sports (thanks to @alp and @adinapoli and other contributors). The idea is that every error message constructor gets assigned an error code of the form GHC-123
. All GHC error messages have the prefix GHC-
; the HF will manage this part of the error code namespace, so that other Haskell tools (e.g. cabal, Haddock, HLS, stack, maybe others) can use this same overall format and infrastructure. This error code will be printed with an error, like this:
Bug.hs:7:34: error GHC-37:
* Couldn't match expected type `Integer' with actual type `Int'
* In the first argument of `f', namely `(6 :: Int)'
In the expression: f (6 :: Int)
In the expression: (f x, f (5 :: Integer), f (6 :: Int))
|
7 | g x = (f x, f (5 :: Integer), f (6 :: Int))
| ^^^^^^^^
Note the GHC-37
; that's the new piece.
To manage the assignment of error codes within GHC, we need two pieces: (a) a way to ensure that all existing error codes are unique and (b) a way to generate a new unique code.
For (a): Every module that prints error messages (that is, defines a Diagnostic
instance) will also include a list of all error codes used by that module. (Technical implementation details are further down.) The module that deals with GhcMessage
(which has injections for all other error message types) will concatenate these lists. We thus will have one list that contains all error codes. This list will power two separate checks:
- In a
DEBUG
compiler, every generated message will check to make sure that its error code is in the list. - A specific new test case will check that the list has no duplicates.
Between these two checks, we can be sure that, for every message produced by the testsuite, its error code is represented in the list and that the list has all unique codes. This still admits the possibility that two messages have the same code, and that code appears only once in the list. One possible way to detect this state is to use Template Haskell to check that the number of error constructors equals the length of the list defined in that module, but this doesn't account for the possibility that we might choose to assign multiple codes to one constructor or have multiple constructors with the same code (this second case should be rare). Given that a failure around uniqueness is not so terrible (it would lead to some confusion when consulting information about error codes), we can evolve this mechanism over time if we need finer control.
For (b): We will write a small utility that looks through the global list of error codes and pseudorandomly produces a new code not in the list. Writers of new codes would use this tool to get their new error code. The seed for the pseudorandom generator will be the name of the current branch that is checked out, so that this tool is deterministic yet will give different patches different answers. There is still the possibility of conflicts (if the generator produces the same code for different users -- unlikely but possible), but these should be caught by the tests above.
Within GHC, this will be implemented by adding a two new methods to Diagnostic
:
class Diagnostic a where
...
diagnosticCode :: a -> Maybe DiagnosticCode
allDiagnosticCodes :: [DiagnosticCode] -- NB: ambiguous type
newtype DiagnosticCode = MkDiagnosticCode Int
We then just add implementations of this method for all known error types. diagnosticCode
returns a Maybe
because not all diagnostics have been structured yet, and it's better to report no code than to report a meaningless one. In the future, we will hopefully remove this Maybe
. The allDiagnosticCodes
returns the list of all codes for constructors of the error type.
Inspiration: Rust's error codes.
I will provide the initial implementation next week, in time for David to use ZuriHac to more widely publicize this effort and to attract volunteers to start writing up error message descriptions. We do not need to merge next week, but we do need to have the implementation stable enough to give accurate error codes. For example, it's possible I won't be able to get the testing infrastructure done in time, but that will be OK for ZuriHac.
The design sketched here will be more fully described in the repo for the eventual website, along with more information about where/how the site will be hosted. David is working on that piece, but the details are not germane for measuring the impact to GHC.
Happy for feedback to this plan, as always.
EDIT: A previous version of this description had a different scheme for avoiding having two users take the same code. This version is better.