Obsługa błędów w ANTLR4

83

Domyślnym zachowaniem, gdy parser nie wie, co zrobić, jest drukowanie wiadomości na terminalu, takich jak:

w wierszu 1:23 brakuje DECIMAL w „}”

To dobra wiadomość, ale w niewłaściwym miejscu. Wolałbym przyjąć to jako wyjątek.

Próbowałem użyć BailErrorStrategy, ale to rzuca ParseCancellationExceptionbez komunikatu (spowodowane przez InputMismatchException, również bez komunikatu).

Czy istnieje sposób, aby zgłosić błędy za pośrednictwem wyjątków, zachowując przydatne informacje w wiadomości?


Oto, czego naprawdę szukam - zwykle używam akcji w regułach, aby zbudować obiekt:

dataspec returns [DataExtractor extractor]
    @init {
        DataExtractorBuilder builder = new DataExtractorBuilder(layout);
    }
    @after {
        $extractor = builder.create();
    }
    : first=expr { builder.addAll($first.values); } (COMMA next=expr { builder.addAll($next.values); })* EOF
    ;

expr returns [List<ValueExtractor> values]
    : a=atom { $values = Arrays.asList($a.val); }
    | fields=fieldrange { $values = values($fields.fields); }
    | '%' { $values = null; }
    | ASTERISK { $values = values(layout); }
    ;

Następnie, kiedy wywołuję parser, robię coś takiego:

public static DataExtractor create(String dataspec) {
    CharStream stream = new ANTLRInputStream(dataspec);
    DataSpecificationLexer lexer = new DataSpecificationLexer(stream);
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    DataSpecificationParser parser = new DataSpecificationParser(tokens);

    return parser.dataspec().extractor;
}

Wszystko, czego naprawdę chcę, to

  • aby dataspec()wywołanie zgłosiło wyjątek (najlepiej zaznaczony), gdy nie można przeanalizować danych wejściowych
  • aby ten wyjątek miał użyteczną wiadomość i zapewniał dostęp do numeru linii i pozycji, w której znaleziono problem

Następnie pozwolę temu wyjątkowi wypłynąć w górę stosu wywołań, gdzie najlepiej nadaje się do przedstawienia użytkownikowi użytecznej wiadomości - w ten sam sposób, w jaki poradziłbym sobie z zerwaniem połączenia sieciowego, odczytaniem uszkodzonego pliku itp.

Widziałem, że akcje są teraz uważane za "zaawansowane" w ANTLR4, więc może zajmuję się rzeczami w dziwny sposób, ale nie sprawdziłem, jaki byłby "niezaawansowany" sposób, aby to zrobić od tego czasu działa dobrze na nasze potrzeby.

Brad Mace
źródło

Odpowiedzi:

98

Ponieważ trochę zmagałem się z dwoma istniejącymi odpowiedziami, chciałbym podzielić się rozwiązaniem, które otrzymałem.

Przede wszystkim stworzyłem własną wersję narzędzia ErrorListener, jak zasugerował Sam Harwell :

public class ThrowingErrorListener extends BaseErrorListener {

   public static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener();

   @Override
   public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e)
      throws ParseCancellationException {
         throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg);
      }
}

Zwróć uwagę na użycie a ParseCancellationExceptionzamiast a, RecognitionExceptionponieważ DefaultErrorStrategy wychwyciłoby to drugie i nigdy nie dotarłoby do twojego własnego kodu.

Tworzenie zupełnie nowej strategii ErrorStrategy, takiej jak sugerował Brad Mace , nie jest konieczne, ponieważ DefaultErrorStrategy generuje domyślnie całkiem dobre komunikaty o błędach.

Następnie używam niestandardowego narzędzia ErrorListener w mojej funkcji analizującej:

public static String parse(String text) throws ParseCancellationException {
   MyLexer lexer = new MyLexer(new ANTLRInputStream(text));
   lexer.removeErrorListeners();
   lexer.addErrorListener(ThrowingErrorListener.INSTANCE);

   CommonTokenStream tokens = new CommonTokenStream(lexer);

   MyParser parser = new MyParser(tokens);
   parser.removeErrorListeners();
   parser.addErrorListener(ThrowingErrorListener.INSTANCE);

   ParserRuleContext tree = parser.expr();
   MyParseRules extractor = new MyParseRules();

   return extractor.visit(tree);
}

(Aby uzyskać więcej informacji na temat tego MyParseRules, co robi, zobacz tutaj ).

To da ci te same komunikaty o błędach, jakie byłyby domyślnie wypisywane na konsoli, tylko w postaci odpowiednich wyjątków.

Mouagip
źródło
3
Spróbowałem tego i potwierdzam, że działało dobrze. Myślę, że jest to najłatwiejsze z 3 proponowanych rozwiązań.
Kami
1
To jest właściwa droga. Najprostszy sposób. „Problem” pojawia się w lekserze i sensowne jest zgłoszenie go od razu, jeśli ważne jest, aby dane wejściowe były poprawne przed próbą analizy. ++
RubberDuck
Czy istnieje konkretny powód, aby używać tej ThrowingErrorListenerklasy jako singletona?
RonyHe
@RonyHe Nie, to tylko adaptacja kodu Sama Harwellsa .
Mouagip,
To rozwiązanie zadziałało dla mnie z jednym zastrzeżeniem - próbujemy przeanalizować za pomocą SLL, a następnie cofamy się do LL i okazuje się, że to nie spowodowało błędu podczas wykonywania analizy rezerwowej. Obejście polegało na skonstruowaniu zupełnie nowego parsera dla drugiej próby zamiast resetowania parsera - najwyraźniej resetowanie parsera nie powoduje zresetowania niektórych ważnych stanów.
Trejkaz
51

W przypadku użycia DefaultErrorStrategylub BailErrorStrategy, ParserRuleContext.exceptionpole jest ustawiane dla dowolnego węzła drzewa analizy w wynikowym drzewie analizy, w którym wystąpił błąd. Dokumentacja dla tego pola brzmi (dla osób, które nie chcą klikać dodatkowego łącza):

Wyjątek, który zmusił tę regułę do powrotu. Jeśli reguła zakończyła się pomyślnie, to jest null.

Edycja: jeśli używasz DefaultErrorStrategy, wyjątek kontekstu analizy nie będzie propagowany aż do kodu wywołującego, więc będziesz mógł exceptionbezpośrednio zbadać pole. Jeśli użyjesz BailErrorStrategy, ParseCancellationExceptionrzucony przez niego będzie zawierał RecognitionExceptionjeśli zadzwonisz getCause().

if (pce.getCause() instanceof RecognitionException) {
    RecognitionException re = (RecognitionException)pce.getCause();
    ParserRuleContext context = (ParserRuleContext)re.getCtx();
}

Edycja 2: Na podstawie innej odpowiedzi wydaje się, że tak naprawdę nie chcesz wyjątku, ale chcesz innego sposobu zgłaszania błędów. W takim przypadku ANTLRErrorListenerinterfejs będzie bardziej zainteresowany . Chcesz wywołać, parser.removeErrorListeners()aby usunąć domyślny odbiornik, który zapisuje na konsoli, a następnie wywołać parser.addErrorListener(listener)własny specjalny odbiornik. Często jako punkt wyjścia używam następującego nasłuchiwania, ponieważ zawiera on nazwę pliku źródłowego z wiadomościami.

public class DescriptiveErrorListener extends BaseErrorListener {
    public static DescriptiveErrorListener INSTANCE = new DescriptiveErrorListener();

    @Override
    public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol,
                            int line, int charPositionInLine,
                            String msg, RecognitionException e)
    {
        if (!REPORT_SYNTAX_ERRORS) {
            return;
        }

        String sourceName = recognizer.getInputStream().getSourceName();
        if (!sourceName.isEmpty()) {
            sourceName = String.format("%s:%d:%d: ", sourceName, line, charPositionInLine);
        }

        System.err.println(sourceName+"line "+line+":"+charPositionInLine+" "+msg);
    }
}

Mając dostępną tę klasę, możesz użyć następujących elementów.

lexer.removeErrorListeners();
lexer.addErrorListener(DescriptiveErrorListener.INSTANCE);
parser.removeErrorListeners();
parser.addErrorListener(DescriptiveErrorListener.INSTANCE);

O wiele bardziej skomplikowanym przykładem detektora błędów, którego używam do identyfikowania niejednoznaczności, które powodują, że gramatyka nie jest SLL, jest SummarizingDiagnosticErrorListenerklasa wTestPerformance .

Sam Harwell
źródło
Ok ... jak to jednak wykorzystać? Czy mam użyć czegoś takiego jak, ((InputMismatchException) pce.getCause()).getCtx().exceptionaby uzyskać przydatny komunikat o błędzie?
Brad Mace
1
Trochę poeksperymentowałem z rzucaniem wyjątku od nasłuchiwania błędów, ale wyjątek nigdy się nie pojawia. Właśnie skończyłem z NPE z działań gramatycznych z powodu nieudanych dopasowań. Dodałem trochę historii do pytania, ponieważ wydaje się, że mogę pływać pod prąd.
Brad Mace
Powinieneś po prostu napisać klasę narzędziową, aby zwrócić „wiersz”, „kolumnę” i „komunikat” z pliku RecognitionException. Żądane informacje są dostępne w wyjątku, który już został zgłoszony.
Sam Harwell,
Delikatny czytelniku, jeśli jesteś podobny do mnie, zastanawiasz się, o co chodzi w REPORT_SYNTAX_ERRORS. Oto odpowiedź: stackoverflow.com/questions/18581880/handling-errors-in-antlr-4
james.garriss
Ten przykład jest naprawdę przydatny. Myślę, że powinno być gdzieś w oficjalnej dokumentacji , brakuje strony do obsługi błędów. Przynajmniej wspomnienie o słuchaczach błędów byłoby dobre.
geekley
10

To, co do tej pory wymyśliłem, opiera się na rozszerzaniu DefaultErrorStrategyi zastępowaniu jej reportXXXmetod (chociaż jest całkiem możliwe, że robię rzeczy bardziej skomplikowane niż to konieczne):

public class ExceptionErrorStrategy extends DefaultErrorStrategy {

    @Override
    public void recover(Parser recognizer, RecognitionException e) {
        throw e;
    }

    @Override
    public void reportInputMismatch(Parser recognizer, InputMismatchException e) throws RecognitionException {
        String msg = "mismatched input " + getTokenErrorDisplay(e.getOffendingToken());
        msg += " expecting one of "+e.getExpectedTokens().toString(recognizer.getTokenNames());
        RecognitionException ex = new RecognitionException(msg, recognizer, recognizer.getInputStream(), recognizer.getContext());
        ex.initCause(e);
        throw ex;
    }

    @Override
    public void reportMissingToken(Parser recognizer) {
        beginErrorCondition(recognizer);
        Token t = recognizer.getCurrentToken();
        IntervalSet expecting = getExpectedTokens(recognizer);
        String msg = "missing "+expecting.toString(recognizer.getTokenNames()) + " at " + getTokenErrorDisplay(t);
        throw new RecognitionException(msg, recognizer, recognizer.getInputStream(), recognizer.getContext());
    }
}

Spowoduje to zgłoszenie wyjątków z przydatnymi komunikatami, a wiersz i położenie problemu można uzyskać z offendingtokenu lub, jeśli nie jest ustawiony, z currenttokenu za pomocą polecenia ((Parser) re.getRecognizer()).getCurrentToken()na RecognitionException.

Jestem dość zadowolony z tego, jak to działa, chociaż mając sześć reportXmetod zastąpienia, myślę, że istnieje lepszy sposób.

Brad Mace
źródło
działa lepiej dla C #, zaakceptowana i najwyżej oceniona odpowiedź miała błędy kompilacji w C #, pewna niekompatybilność argumentu generycznego IToken vs int
sarh
0

Dla wszystkich zainteresowanych, oto odpowiednik odpowiedzi Sama Harwella w języku ANTLR4 C #:

using System; using System.IO; using Antlr4.Runtime;
public class DescriptiveErrorListener : BaseErrorListener, IAntlrErrorListener<int>
{
  public static DescriptiveErrorListener Instance { get; } = new DescriptiveErrorListener();
  public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) {
    if (!REPORT_SYNTAX_ERRORS) return;
    string sourceName = recognizer.InputStream.SourceName;
    // never ""; might be "<unknown>" == IntStreamConstants.UnknownSourceName
    sourceName = $"{sourceName}:{line}:{charPositionInLine}";
    Console.Error.WriteLine($"{sourceName}: line {line}:{charPositionInLine} {msg}");
  }
  public override void SyntaxError(TextWriter output, IRecognizer recognizer, Token offendingSymbol, int line, int charPositionInLine, string msg, RecognitionException e) {
    this.SyntaxError(output, recognizer, 0, line, charPositionInLine, msg, e);
  }
  static readonly bool REPORT_SYNTAX_ERRORS = true;
}
lexer.RemoveErrorListeners();
lexer.AddErrorListener(DescriptiveErrorListener.Instance);
parser.RemoveErrorListeners();
parser.AddErrorListener(DescriptiveErrorListener.Instance);
geekley
źródło