Обробка помилок у ANTLR4


83

Поведінка за замовчуванням, коли синтаксичний аналізатор не знає, що робити, це друкувати повідомлення на терміналі, наприклад:

рядок 1:23 відсутній DECIMAL на '}'

Це гарне повідомлення, але не в тому місці. Я волів би отримати це як виняток.

Я спробував використовувати BailErrorStrategy, але це видає a ParseCancellationExceptionбез повідомлення (викликане a InputMismatchException, також без повідомлення).

Чи можу я змусити його повідомляти про помилки через винятки, зберігаючи корисну інформацію в повідомленні?


Ось що я насправді шукаю - я зазвичай використовую дії в правилах для побудови об’єкта:

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); }
    ;

Потім, коли я викликаю парсер, я роблю щось подібне:

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;
}

Все, що я справді хочу, - це

  • для dataspec()виклику видавати виняток (в ідеалі перевірений), коли вхід неможливо проаналізувати
  • щоб цей виняток мав корисне повідомлення та забезпечував доступ до номера рядка та місця, де було виявлено проблему

Тоді я дозволю цьому винятку створити бульбашку, де б там не було, якнайкраще підходить для подання корисного повідомлення користувачеві - так само, як я б обробляв розірване мережеве підключення, читання пошкодженого файлу тощо.

Я бачив, що дії зараз вважаються "просунутими" в ANTLR4, тому, можливо, я дивлюся на щось дивно, але я не вивчав, яким би був "непросунутий" спосіб зробити це, оскільки таким чином добре працює для наших потреб.

Відповіді:


98

Оскільки я трохи боровся з двома існуючими відповідями, я хотів би поділитися рішенням, з яким я закінчив.

Перш за все я створив власну версію ErrorListener, як запропонував Сем Гарвелл :

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);
      }
}

Зверніть увагу на використання ParseCancellationExceptionзамість a, RecognitionExceptionоскільки DefaultErrorStrategy схопить останню, і вона ніколи не досягне вашого власного коду.

Створення цілком нової ErrorStrategy, як запропонував Бред Мейс , не є необхідним, оскільки DefaultErrorStrategy за замовчуванням видає досить хороші повідомлення про помилки.

Потім я використовую власний ErrorListener у своїй функції аналізу:

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);
}

(Докладніше про те, що MyParseRulesробить, дивіться тут .)

Це дасть вам ті самі повідомлення про помилки, що друкуються на консолі за замовчуванням, лише у формі належних винятків.


3
Я спробував це і підтверджую, що це спрацювало добре. Я думаю, це найпростіший із 3 запропонованих рішень.
Камі

1
Це правильний шлях. Найпростіший шлях. "Проблема" трапляється в лексері, і має сенс повідомити про неї тут же і там, якщо важливо, щоб введення було дійсним перед спробою аналізу. ++
RubberDuck

Чи є особлива причина використовувати ThrowingErrorListenerклас як синглтона?
RonyHe

@RonyHe Ні, це лише адаптація коду Сема Гарвеллса .
Mouagip

Це рішення спрацювало для мене з одним застереженням - ми намагаємося провести синтаксичний аналіз за допомогою SLL, а потім повернутися до LL, і виявляється, що це не спричинило жодної помилки при виконанні резервного аналізу. Обхідним шляхом було створення повністю нового синтаксичного аналізатора для другої спроби замість скидання синтаксичного аналізатора - очевидно, скидання синтаксичного аналізатора не дозволяє скинути деякі важливі стани.
Трейказ

51

Коли ви використовуєте DefaultErrorStrategyабо BailErrorStrategy, ParserRuleContext.exceptionполе встановлюється для будь-якого вузла дерева синтаксичного аналізу в отриманому дереві синтаксичного аналізу, де сталася помилка. Документація до цього поля читає (для людей, які не хочуть натискати додаткове посилання):

Виняток, який змусив це правило повернутися. Якщо правило успішно виконано, це так null.

Змінити: якщо ви використовуєте DefaultErrorStrategy, виняток контексту синтаксичного аналізу не буде розповсюджений до викличного коду, тож ви зможете дослідити exceptionполе безпосередньо. Якщо ви використовуєте BailErrorStrategy, ParseCancellationExceptionкинуте ним буде включати в себе, RecognitionExceptionякщо ви телефонуєте getCause().

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

Редагування 2: Виходячи з вашої іншої відповіді, виявляється, що ви насправді не хочете виключення, але те, що ви хочете, - це інший спосіб повідомити про помилки. У цьому випадку вас більше зацікавить ANTLRErrorListenerінтерфейс. Ви хочете зателефонувати, parser.removeErrorListeners()щоб видалити прослуховувач за замовчуванням, який пише на консолі, а потім зателефонувати parser.addErrorListener(listener)для власного спеціального прослуховувача. Я часто використовую наступний прослуховувач як вихідну точку, оскільки він включає ім'я вихідного файлу з повідомленнями.

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);
    }
}

Якщо цей клас доступний, ви можете використовувати наступне, щоб використовувати його.

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

Набагато більш складний приклад слухача помилки , які я використовую , щоб визначити двозначності , які роблять граматику , НЕ SLL є SummarizingDiagnosticErrorListenerкласTestPerformance .


Гаразд ... як мені цим скористатися? Чи повинен я використовувати щось на зразок, ((InputMismatchException) pce.getCause()).getCtx().exceptionщоб отримати корисне повідомлення про помилку?
Бред Мейс,

1
Я трохи експериментував з викиданням винятку із прослуховувача помилок, але, здається, виняток ніколи не з’являється. Я щойно опинився в NPE з граматичних дій через невдалі збіги. Я додав кілька попередніх питань до запитання, оскільки, схоже, я, можливо, плаваю проти течії.
Brad Mace

Вам слід просто написати клас корисності, щоб повернути "рядок", "стовпець" і "повідомлення" з RecognitionException. Інформація, яку ви хочете, доступна за винятком, який вже викидається.
Сем Гарвелл

Ніжний читачу, якщо ти схожий на мене, то ти задаєшся питанням, що таке REPORT_SYNTAX_ERRORS. Ось відповідь: stackoverflow.com/questions/18581880/handling-errors-in-antlr-4
james.garriss

Цей приклад дуже корисний. Я думаю, це повинно бути десь в офіційній документації , здається, бракує сторінки для обробки помилок. Хоча б згадати слухачів помилок, було б непогано.
geekley

10

Те, що я придумав дотепер, базується на розширенні DefaultErrorStrategyта перевизначенні його reportXXXметодів (хоча цілком можливо, я роблю речі більш складними, ніж потрібно):

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());
    }
}

Це викидає винятки з корисними повідомленнями, і рядок та положення проблеми можна отримати або з offendingмаркера, або якщо це не встановлено, з currentмаркера, використовуючи ((Parser) re.getRecognizer()).getCurrentToken()на RecognitionException.

Я досить задоволений тим, як це працює, хоча наявність шести reportXметодів, які можна замінити, змушує мене думати, що є кращий спосіб.


працює краще для c #, прийнята та найголовніша відповідь мала помилки компіляції в c #, деяка несумісність загального аргументу IToken vs int
sarh

0

Для всіх, хто цікавиться, ось 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);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.