Antlr is awesome, however, one caveat of it is that it is not idiomatic Go when it comes to error handling. This makes the whole error process unintuitive to a GoLang engineer.
In order to inject your own error handling at each step (lexing, parsing, walking), you have to inject error listeners/handlers with panics. Panic & recovery is very much like a Java exception, and I think that is why it is designed this way (Antlr is written in Java).
Lex/Parse Error Collecting (easy to do)
You can implement as many ErrorListeners as you'd like. The default one used is ConsoleErrorListenerInstance
. All it does is print to stderr on SyntaxErrors, so we remove it. The first step to custom error reporting is to replace this. I made a basic one that just collects the errors in a custom type that I can use/report with later.
type CustomSyntaxError struct {
line, column int
msg string
}
type CustomErrorListener struct {
*antlr.DefaultErrorListener // Embed default which ensures we fit the interface
Errors []error
}
func (c *CustomErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) {
c.Errors = append(c.Errors, &CustomSyntaxError{
line: line,
column: column,
msg: msg,
})
}
You can inject the error listener (while clearing that default one) on the parser/lexer.
lexerErrors := &CustomErrorListener{}
lexer := NewMyLexer(is)
lexer.RemoveErrorListeners()
lexer.AddErrorListener(lexerErrors)
parserErrors := &CustomErrorListener{}
parser := NewMyParser(stream)
p.removeErrorListeners()
p.AddErrorListener(parserErrors)
When the Lexing/Parsing finishes, both data structures will have the syntax errors found during the Lexing/Parsing stage. You can play around with the fields given in SyntaxError
. You'll have to look elsewhere for the other interface functions like ReportAmbuiguity
.
if len(lexerErrors.Errors) > 0 {
fmt.Printf("Lexer %d errors found
", len(lexerErrors.Errors))
for _, e := range lexerErrors.Errors {
fmt.Println("", e.Error())
}
}
if len(parserErrors.Errors) > 0 {
fmt.Printf("Parser %d errors found
", len(parserErrors.Errors))
for _, e := range parserErrors.Errors {
fmt.Println("", e.Error())
}
}
Lex/Parse Error Aborting (unsure how solid this is)
WARNING: This really feels jank. If just error collecting is needed, just do what was shown above!
To abort a lex/parse midway, you have to throw a panic in the error listener. I don't get this design to be honest, but the lexing/parsing code is wrapped in panic recovers that check if the panic is of the type RecognitionException
. This Exception is passed as an argument to your ErrorListener
, so modify the SyntaxError
expression
func (c *CustomErrorListener) SyntaxError(recognizer antlr.Recognizer, offendingSymbol interface{}, line, column int, msg string, e antlr.RecognitionException) {
// ...
panic(e) // Feel free to only panic on certain conditions. This stops parsing/lexing
}
This panic error is caught and passed to the ErrorHandler
which implements ErrorStrategy
. The important function we care about is Recover()
. Recover attempts to recover from the error, consuming the token stream until the expected pattern/token can be found. Since we want this to abort, we can take inspiration from BailErrorStrategy
. This strategy still sucks, as it uses panics to stop all work. You can simply just omit the implementation.
type BetterBailErrorStrategy struct {
*antlr.DefaultErrorStrategy
}
var _ antlr.ErrorStrategy = &BetterBailErrorStrategy{}
func NewBetterBailErrorStrategy() *BetterBailErrorStrategy {
b := new(BetterBailErrorStrategy)
b.DefaultErrorStrategy = antlr.NewDefaultErrorStrategy()
return b
}
func (b *BetterBailErrorStrategy) ReportError(recognizer antlr.Parser, e antlr.RecognitionException) {
// pass, do nothing
}
func (b *BetterBailErrorStrategy) Recover(recognizer antlr.Parser, e antlr.RecognitionException) {
// pass, do nothing
}
// Make sure we don't attempt to recover from problems in subrules.//
func (b *BetterBailErrorStrategy) Sync(recognizer antlr.Parser) {
// pass, do nothing
}
Then add to the parser
parser.SetErrorHandler(NewBetterBailErrorStrategy())
That being said, I'd advise just collecting the errors with the listeners, and not bother trying to abort early. The BailErrorStrategy
doesn't really seem to work all that well, and the use of panics to recover feels so clunky in GoLang, it's easy to mess up.