Separating typechecking and type error reporting in two passes?

Hi, the following is a rough idea that I came up with while pondering #12864, which is about better (and fewer!) error messages when the user forgot an argument to a function, or swapped their order. The current scheme of type checking and error reporting is the following (please correct me if I am wrong, I have actually done little in this area so far): The typechecker traverses the syntax tree from top to bottom. Along the way, it keeps track of the current context in terms of SDoc fragments of the form “In the second argument of…”. It generates constraints (e.g. the argument type of a function is the type of the actual argument), which it either solves, or propagate outwards as “wanted constraints”, which refer to their context, and refer to a “hole” in the program where, if this constraint gets solved later, some form of evidence gets filled in. At the end, if there are any wanted constraints left, they are reported as type errors, together with the context they are mentioned. But with -fdefer-type-errors, they are not reported, but the evidence holes are filled in with essentially calls to `error` that print the error message at runtime. The problem I see with this approach is that the type checker has to remember interesting bits about context that might possibly eventually be relevant when reporting the error. In particular, it makes it harder to report multiple related error messages at once. So I am wondering if this approach would be better: The type checker does *not* bother keeping track of context: It traverses the syntax tree, creates constraints, and unsolvable constraints get filled with “error evidence”. Basically as if -fdefer-type-errors is one. Then a second pass is done over the syntax tree. This pass does keep track of the context. Whenever it finds some error evidence, it reports it. I see the following advantages: * The type checker code gets a bit simpler and tidier. * As most compiled programs are type correct [citation needed], the type checker, but not the error reporting, is compiler-performance- critical. This might make type checking a (possibly insignificant) tad faster. * The error reporting pass can “look around” more. For example, before recursing into a pair `(e1,e2)`, it could check for the special case of `(e1 ▷ TyError Int Bool, e1 ▷ TyError Bool Int)` and report this as one error “Tuple with swapped arguments” instead of two errors. The #12864 is a more elaborate application of this. * There could even be an intermediate pass over the syntax tree between typechecking and error reporting that “transform” or “optimizes” the AST by moving type errors around, by coalescing error or other things, all with the aim of giving easier to understand error messages. * (This is getting into the area of wild dreams:) Library authors could somehow express custom error messages for certain patterns of the form If the AST now contains an application of `Library.foo` where the first argument is a type error `TyError Int Bool`, then give this particular domain-specific error message. I don’t see any disadvantages now, but there probably are some, and I would be grateful about feedback from those who actually have worked with this part of GHC before. Thanks, Joachim -- Joachim “nomeata” Breitner mail@joachim-breitner.de • https://www.joachim-breitner.de/ XMPP: nomeata@joachim-breitner.de • OpenPGP-Key: 0xF0FBF51F Debian Developer: nomeata@debian.org

One concern I have is with the claim that "most compiled programs are type
correct". This has emphatically not been my experience while developing
Haskell. Often, I edit and recompile to find the next type error to fix, or
the new type of the hole; this is especially easy (and automatic) in emacs
with flycheck.
It's also the case that compiling correct programs is (or at least appears
to me to be) much slower than compiling incorrect ones (with GHC 8.0.1, at
least). I worry that this proposal might slow down type errors
significantly. Of course, someone with more (basically any) knowledge of
GHC internals can correct me and allay my fear.
On Tue, Nov 29, 2016 at 8:20 AM Joachim Breitner
Hi,
the following is a rough idea that I came up with while pondering #12864, which is about better (and fewer!) error messages when the user forgot an argument to a function, or swapped their order.
The current scheme of type checking and error reporting is the following (please correct me if I am wrong, I have actually done little in this area so far):
The typechecker traverses the syntax tree from top to bottom. Along the way, it keeps track of the current context in terms of SDoc fragments of the form “In the second argument of…”. It generates constraints (e.g. the argument type of a function is the type of the actual argument), which it either solves, or propagate outwards as “wanted constraints”, which refer to their context, and refer to a “hole” in the program where, if this constraint gets solved later, some form of evidence gets filled in. At the end, if there are any wanted constraints left, they are reported as type errors, together with the context they are mentioned. But with -fdefer-type-errors, they are not reported, but the evidence holes are filled in with essentially calls to `error` that print the error message at runtime.
The problem I see with this approach is that the type checker has to remember interesting bits about context that might possibly eventually be relevant when reporting the error. In particular, it makes it harder to report multiple related error messages at once.
So I am wondering if this approach would be better:
The type checker does *not* bother keeping track of context: It traverses the syntax tree, creates constraints, and unsolvable constraints get filled with “error evidence”. Basically as if -fdefer-type-errors is one.
Then a second pass is done over the syntax tree. This pass does keep track of the context. Whenever it finds some error evidence, it reports it.
I see the following advantages:
* The type checker code gets a bit simpler and tidier. * As most compiled programs are type correct [citation needed], the type checker, but not the error reporting, is compiler-performance- critical. This might make type checking a (possibly insignificant) tad faster. * The error reporting pass can “look around” more. For example, before recursing into a pair `(e1,e2)`, it could check for the special case of `(e1 ▷ TyError Int Bool, e1 ▷ TyError Bool Int)` and report this as one error “Tuple with swapped arguments” instead of two errors. The #12864 is a more elaborate application of this. * There could even be an intermediate pass over the syntax tree between typechecking and error reporting that “transform” or “optimizes” the AST by moving type errors around, by coalescing error or other things, all with the aim of giving easier to understand error messages. * (This is getting into the area of wild dreams:) Library authors could somehow express custom error messages for certain patterns of the form If the AST now contains an application of `Library.foo` where the first argument is a type error `TyError Int Bool`, then give this particular domain-specific error message.
I don’t see any disadvantages now, but there probably are some, and I would be grateful about feedback from those who actually have worked with this part of GHC before.
Thanks, Joachim
-- Joachim “nomeata” Breitner mail@joachim-breitner.de • https://www.joachim-breitner.de/ XMPP: nomeata@joachim-breitner.de • OpenPGP-Key: 0xF0FBF51F Debian Developer: nomeata@debian.org _______________________________________________ ghc-devs mailing list ghc-devs@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs

Hi, I guess the claim is still true: Think about just the amount of code you compile when you install your dependencies. But you are right that when the programmer sits there and waits for a result, that’s when snappyness is important. I don’t expect this change to be human-noticable in terms of performance, and the prospect of getting more useful error messages (which are easier to grasp and fix) will certainly outweigh that. Anyways, performance aspects are just a side-effect of this. Greetings, Joachim Am Dienstag, den 29.11.2016, 16:48 +0000 schrieb Alex Rozenshteyn:
One concern I have is with the claim that "most compiled programs are type correct". This has emphatically not been my experience while developing Haskell. Often, I edit and recompile to find the next type error to fix, or the new type of the hole; this is especially easy (and automatic) in emacs with flycheck.
It's also the case that compiling correct programs is (or at least appears to me to be) much slower than compiling incorrect ones (with GHC 8.0.1, at least). I worry that this proposal might slow down type errors significantly. Of course, someone with more (basically any) knowledge of GHC internals can correct me and allay my fear.
On Tue, Nov 29, 2016 at 8:20 AM Joachim Breitner
wrote: Hi,
the following is a rough idea that I came up with while pondering #12864, which is about better (and fewer!) error messages when the user forgot an argument to a function, or swapped their order.
The current scheme of type checking and error reporting is the following (please correct me if I am wrong, I have actually done little in this area so far):
The typechecker traverses the syntax tree from top to bottom. Along the way, it keeps track of the current context in terms of SDoc fragments of the form “In the second argument of…”. It generates constraints (e.g. the argument type of a function is the type of the actual argument), which it either solves, or propagate outwards as “wanted constraints”, which refer to their context, and refer to a “hole” in the program where, if this constraint gets solved later, some form of evidence gets filled in. At the end, if there are any wanted constraints left, they are reported as type errors, together with the context they are mentioned. But with -fdefer-type-errors, they are not reported, but the evidence holes are filled in with essentially calls to `error` that print the error message at runtime.
The problem I see with this approach is that the type checker has to remember interesting bits about context that might possibly eventually be relevant when reporting the error. In particular, it makes it harder to report multiple related error messages at once.
So I am wondering if this approach would be better:
The type checker does *not* bother keeping track of context: It traverses the syntax tree, creates constraints, and unsolvable constraints get filled with “error evidence”. Basically as if -fdefer-type-errors is one.
Then a second pass is done over the syntax tree. This pass does keep track of the context. Whenever it finds some error evidence, it reports it.
I see the following advantages:
* The type checker code gets a bit simpler and tidier. * As most compiled programs are type correct [citation needed], the type checker, but not the error reporting, is compiler- performance- critical. This might make type checking a (possibly insignificant) tad faster. * The error reporting pass can “look around” more. For example, before recursing into a pair `(e1,e2)`, it could check for the special case of `(e1 ▷ TyError Int Bool, e1 ▷ TyError Bool Int)` and report this as one error “Tuple with swapped arguments” instead of two errors. The #12864 is a more elaborate application of this. * There could even be an intermediate pass over the syntax tree between typechecking and error reporting that “transform” or “optimizes” the AST by moving type errors around, by coalescing error or other things, all with the aim of giving easier to understand error messages. * (This is getting into the area of wild dreams:) Library authors could somehow express custom error messages for certain patterns of the form If the AST now contains an application of `Library.foo` where the first argument is a type error `TyError Int Bool`, then give this particular domain-specific error message.
I don’t see any disadvantages now, but there probably are some, and I would be grateful about feedback from those who actually have worked with this part of GHC before.
Thanks, Joachim
-- Joachim “nomeata” Breitner mail@joachim-breitner.de • https://www.joachim-breitner.de/ XMPP: nomeata@joachim-breitner.de • OpenPGP-Key: 0xF0FBF51F Debian Developer: nomeata@debian.org_____________________________ __________________ ghc-devs mailing list ghc-devs@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs
_______________________________________________ ghc-devs mailing list ghc-devs@haskell.org http://mail.haskell.org/cgi-bin/mailman/listinfo/ghc-devs -- Joachim Breitner mail@joachim-breitner.de http://www.joachim-breitner.de/

Il giorno 29 nov 2016, alle ore 17:52, Joachim Breitner
ha scritto: Hi,
I guess the claim is still true: Think about just the amount of code you compile when you install your dependencies.
But you are right that when the programmer sits there and waits for a result, that’s when snappyness is important. I don’t expect this change to be human-noticable in terms of performance, and the prospect of getting more useful error messages (which are easier to grasp and fix) will certainly outweigh that.
Anyways, performance aspects are just a side-effect of this.
Hi all, the following is a comment from an interested outsider. I’ve never hacked on GHC, but I have some little experience with clang internals. Compiling C++ code is a very heavy task, especially when a lot of templates are involved, and the design of the whole compiler is particularly performance-driven. As you may know, they nevertheless put a lot of effort on the understandability of compilation errors. As a coding guideline, they treat the compilation of correct code as the “fast path”, and the error reporting as the slow path. This doesn’t mean the compiler is slow at reporting errors: as much as you can go around the AST to collect information, you won’t ever be able to slow it so much as to outweigh the time to codegen the code if it were correct. So at the end, the programmer experiences a fast iteration anyway. All of this to say that my humble experience with clang suggests that an improvement in error messages is worth a little slow down in the incorrect code compilation path, and it is a very important goal to pursue even if it had to slow down some micro benchmark. Greetings, Nicola

But you are right that when the programmer sits there and waits for a result, that’s when snappyness is important.
I had a random idea based on this observation: (With a certain flag set) the compiler could follow the existing strategy until it has hit the first n errors, possibly with n=1. Then it could switch off the context overhead and all subsequent errors could be deferred or not fleshed out. Or, alternatively, the proposed new strategy is used, but the second pass only looks at the first n errors. Benefit: Correct code is on the fast path, but error reporting doesn't add too much of an overhead. My experience when using the compiler to have a conversation about errors was that I was correcting one or two errors at a time, then re-compiling. I discarded all the extra information about the other errors anyway, at least most of the time. I don't know if that is a usual pattern, but if it is we might as well exploit it. This idea could already benefit from a separation, but we can go further. What if, in interactive sessions, you would only get the result of the first pass at first. No details, but only a list of error positions. In some cases, that is all you need to find a dumb typo. It also doesn't clutter the screen with loads of fluff while still giving you a basic idea of how much is wrong. Now what if you could then instruct the system to do the second pass at places you choose, interactively? In other words the conversation would be even more conversational. Of course the benefits are debatable and this is not something that's going to be happening soon anyway. But for me the idea alone is an argument for the proposed new separation, because it would give us the flexibility to think of features like this. Cheers, MarLinn

Hi, Am Mittwoch, den 30.11.2016, 12:59 +0100 schrieb MarLinn via ghc-devs:
But you are right that when the programmer sits there and waits for a result, that’s when snappyness is important.
I had a random idea based on this observation:
please allow me to re-iterate that my proposal is *not* about performance, but about being able to detect and report typical error patterns such as confusing the order of arguments to a function, or to leave out one argument. (Wouldn’t that be great?) I regret pointing out hypothetical and likely insignificant performance effects; it derails the discussion. Your ideas (printing less errors, or less details, at first until asked) can be pursued already now. There is precedent, e.g. https://ghc.haskell.org/trac/ghc/ticket/10848 which added the -freverse-errors option. Options in similar vain (-fone-line-errors, -fnum-errors=1) are worth considering and can be implemented today, so if you feel like it, do explain your ideas in greater detail. Greetings, Joachim -- Joachim “nomeata” Breitner mail@joachim-breitner.de • https://www.joachim-breitner.de/ XMPP: nomeata@joachim-breitner.de • OpenPGP-Key: 0xF0FBF51F Debian Developer: nomeata@debian.org

| Then a second pass is done over the syntax tree. This pass does | keep | track of the context. Whenever it finds some error evidence, it | reports it. The syntax tree is a big type. A second pass would be a fairly big deal. But doable. You'd need to be able to look at the nature of the error; SDocs won't do. Maybe errors become a data type. That might be a good idea but it would be another big deal. I very much doubt that you'll be able to discard the context information from the type checker. Maybe some of it. I can't say exactly why, it's a gut feel for now. There is a rich literature on type errors, worth digging into. Alejandro Serrano and his adviser Juraiaan Hage are thinking about this at the moment. SherLoc was interesting too; worked for Haskell. Lennart gave a nice talk at the Haskell Implementors meeting last year about error provenance. Maybe available online? Simon | -----Original Message----- | From: ghc-devs [mailto:ghc-devs-bounces@haskell.org] On Behalf Of | Joachim Breitner | Sent: 29 November 2016 16:20 | To: ghc-devs@haskell.org | Subject: Separating typechecking and type error reporting in two | passes? | | Hi, | | the following is a rough idea that I came up with while pondering | #12864, which is about better (and fewer!) error messages when the | user forgot an argument to a function, or swapped their order. | | | The current scheme of type checking and error reporting is the | following (please correct me if I am wrong, I have actually done | little in this area so far): | | The typechecker traverses the syntax tree from top to bottom. | Along | the way, it keeps track of the current context in terms of SDoc | fragments of the form “In the second argument of…”. It generates | constraints (e.g. the argument type of a function is the type of | the | actual argument), which it either solves, or propagate outwards as | “wanted constraints”, which refer to their context, and refer to a | “hole” in the program where, if this constraint gets solved later, | some form of evidence gets filled in. | At the end, if there are any wanted constraints left, they are | reported as type errors, together with the context they are | mentioned. | But with -fdefer-type-errors, they are not reported, but the | evidence holes are filled in with essentially calls to `error` | that | print the error message at runtime. | | The problem I see with this approach is that the type checker has to | remember interesting bits about context that might possibly eventually | be relevant when reporting the error. In particular, it makes it | harder to report multiple related error messages at once. | | | So I am wondering if this approach would be better: | | The type checker does *not* bother keeping track of context: It | traverses the syntax tree, creates constraints, and unsolvable | constraints get filled with “error evidence”. Basically as if | -fdefer-type-errors is one. | | Then a second pass is done over the syntax tree. This pass does | keep | track of the context. Whenever it finds some error evidence, it | reports it. | | | I see the following advantages: | | * The type checker code gets a bit simpler and tidier. | * As most compiled programs are type correct [citation needed], the | type checker, but not the error reporting, is compiler-performance- | critical. This might make type checking a (possibly insignificant) | tad faster. | * The error reporting pass can “look around” more. For example, | before | recursing into a pair `(e1,e2)`, it could check for the special | case | of `(e1 ▷ TyError Int Bool, e1 ▷ TyError Bool Int)` and report this | as one error “Tuple with swapped arguments” instead of two errors. | The #12864 is a more elaborate application of this. | * There could even be an intermediate pass over the syntax tree | between typechecking and error reporting that “transform” or | “optimizes” the AST by moving type errors around, by coalescing | error or other things, all with the aim of giving easier to | understand error messages. | * (This is getting into the area of wild dreams:) | Library authors could somehow express custom error messages | for certain patterns of the form | If the AST now contains an application of `Library.foo` where | the first argument is a type error `TyError Int Bool`, then | give this particular domain-specific error message. | | I don’t see any disadvantages now, but there probably are some, and I | would be grateful about feedback from those who actually have worked | with this part of GHC before. | | Thanks, | Joachim | | -- | Joachim “nomeata” Breitner | mail@joachim-breitner.de • | https://na01.safelinks.protection.outlook.com/?url=https%3A%2F%2Fwww.j | oachim- | breitner.de%2F&data=02%7C01%7Csimonpj%40microsoft.com%7C41dcebe5f26043 | 94f94108d41873a1f0%7C72f988bf86f141af91ab2d7cd011db47%7C1%7C0%7C636160 | 332322948063&sdata=hoylWVvBSQjqGmm0ICnr3nDFz80waxDStDOjWDPQb5A%3D&rese | rved=0 | XMPP: nomeata@joachim-breitner.de • OpenPGP-Key: 0xF0FBF51F | Debian Developer: nomeata@debian.org

Hi, Am Mittwoch, den 30.11.2016, 10:51 +0000 schrieb Simon Peyton Jones via ghc-devs:
I very much doubt that you'll be able to discard the context information from the type checker. Maybe some of it. I can't say exactly why, it's a gut feel for now.
the gut feeling is warranted: There are quite a few errors that are raised directly by the type checker, and not expressed as a WantedConstraint that can be deferred. Kind errors inside types, for example. It might be an interesting exercise to try to make all errors deferrable somehow. With some additions to the relevant types (HsType etc.) this might work. But until then, a second pass would have to *duplicate* all the context-handling machinery, and that is surely not the way to go. Punting this for now, for want of a better idea. Greetings, Joachim -- Joachim “nomeata” Breitner mail@joachim-breitner.de • https://www.joachim-breitner.de/ XMPP: nomeata@joachim-breitner.de • OpenPGP-Key: 0xF0FBF51F Debian Developer: nomeata@debian.org
participants (5)
-
Alex Rozenshteyn
-
Joachim Breitner
-
MarLinn
-
Nicola Gigante
-
Simon Peyton Jones