I thought a lot about compilers these past few days. One challenge that arises in the front-end is to provide the user with useful error messages: where the error was detected, where it occured, how to fix it, etc.
The common pratice in compilers that I know is to use a reporter
to signal the error. The reporter is provider with
an error string and is expected to display it to the user. So we have user-facing formatted strings in the source code :-(
For instance, this snippet is from llvm's lexer:
if (NumBits < IntegerType::MIN_INT_BITS ||
NumBits > IntegerType::MAX_INT_BITS) {
Error("bitwidth for integer type out of range!");
return lltok::Error;
}
[source] lexer: https://github.com/llvm-mirror/llvm/blob/master/lib/AsmParser/LLLexer.cpp
and this snippet was found in the scala compiler:
if (settings.warnUnsoundMatch && unsoundAssumptionUsed)
reporter.warning(pat.pos,
sm"""The value matched by $pat is bound to ${binder.name}, which may be used under the
|unsound assumption that it has type ${pat.tpe}, whereas we can only safely
|count on it having type $pt, as the pattern is matched using `==` (see SI-1503).""")
pat.tpe
}
[source] match analysis: https://github.com/scala/scala/blob/2.12.x/src/compiler/scala/tools/nsc/transform/patmat/MatchAnalysis.scala
Now, another approach would be to have an object for each possible error. By localizing the objects in the same file or directory, we would have a list of existing errors, and it would be easy to modify them all at once. This also keeps the code clean, and, more importantly we can provide as much information as needed while creating the object, even if it's not displayed (for now) to the user.
For instance:
if (settings.warnUnsoundMatch && unsoundAssumptionUsed)
reporter.signal(new SI1503(pat, binder, pt))
pat.tpe
}