7
$\begingroup$

What are some good ways to report errors when compiling code in a new programming language? One answer may be to just stop after the first error, but this can be unsatisfactory because it takes additional time to fix the error, re-compile, and then fix the next error. In contrast, if the language implementation reports all the errors in advance, the programmer can fix them all in one go and save several seconds of time spent waiting on the program to build.

To clarify what I'm asking, suppose we have the following sample program in C++ that is supposed to print "Hello World". However, this program was not transcribed properly and has a few errors.

#include <iostream>
int main() {
    cout << "Hello World!"
}

Now, our friend the compiler comes along and starts reading this program. First, it will see that we forgot to qualify cout with the namespace (it should be std::cout instead), so the program needs to issue an error. My question is, what code mechanism should the program use to do so? For instance, you may consider a scheme that uses error codes, or a scheme that uses exceptions (but I would expect an answer to go a bit more in-depth). However, after reporting the error, the compiler might want to continue parsing the remainder of the program to report other issues. (It doesn't necessarily have to, but as pointed out above, doing so can be better for the user in that it allows the user to fix multiple errors at once.) It will see that the line of code also doesn't end in a semicolon, so it will again need to report that error as well.

$\endgroup$
0

1 Answer 1

4
$\begingroup$

Diagnostics (error messages, warnings and so on) are data, so you should have a data structure to represent them. This could be a simple struct or class with fields for a message string and a source span (i.e. source file name, line and column numbers); or it could be a big enum with variants for every different kind of diagnostic your compiler can report.

Then all you need is a list of diagnostics, which can be added to during compilation. I like to encapsulate this, to give some helper methods to create each kind of diagnostic object and also so I can check and throw an exception if there are, say, over 100 diagnostics already reported.

Here's a couple of examples from my own compilers:

  • My language MJr is implemented in Typescript, currently I'm just storing diagnostics as strings because that's simple, but I have a helper method to include the source span information in those strings. The source code for the Diagnostics class which collects reported diagnostics in a list is here.
  • My language Papyri is implemented in Rust; diagnostics are structured data including a severity (e.g. is it an error or a warning?) and source span, I have enums for each kind of diagnostic, so e.g. here's all of the type errors, and here's the struct I collect them in.

From there, it's just a matter of reporting diagnostics in the appropriate places. However, since you want to continue compiling even when there are errors in the user's code, you don't want to throw an exception and halt compilation on any error, but there also isn't a "good" result to return. For this, the standard solution is to have a special "poisoned" value which represents a failed compilation for which a diagnostic has already been reported.

For example, if there's a type error so that a + b doesn't make sense, you can't just return an AST node representing an addition of two numbers or a concatenation of two strings, because you don't know which it should be, and you also don't want to report spurious type errors later because the a + b might occur in a context where a number or a string isn't allowed. So instead of returning a proper AST node, you report the type error and return a "poisoned" node. Then later when you're type-checking the parent node which contained that problematic node, you see that the child node is poisoned so you don't need to report another error about it.

Typically, your compiler will have several stages, so e.g. parsing is separate to name resolution and type-checking. Often there is no point progressing to the next stage if an earlier stage reported any errors (though warnings are acceptable), because a source file with syntax errors probably won't type-check either, but it's the syntax which needs to be fixed. So at the end of each stage, you probably want to check if your diagnostics list has any errors before moving to the next stage.

$\endgroup$
2
  • 1
    $\begingroup$ The point made in your last paragraph is important but it might not be clear to the reader why "there's no point". A germane example: if the "is there a cycle in the base classes" checker produces an error, then that's a great time to stop looking for more errors because code that reports, say, illegal conversions probably assumes that the type hierarchy is acylic. If you bail out early then you don't need to make later error passes safe to analyze wrong programs! $\endgroup$ Commented Jul 13, 2023 at 6:46
  • $\begingroup$ Regarding last paragraph: it might not be useful for compiler, but for the langauge server it is extremely useful to report as much diagnostics as possible (with some certainty of correctness). LSP showing a single syntax error on missing semicolon is a bad LSP. $\endgroup$ Commented Aug 15 at 1:06

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .