Синтаксические предикаты в правилах лексера ANTLR
Вступление
Глядя на документацию, в ANTLR 2 использовалось нечто, называемое предикатным лексингом, с примерами, подобными этому (вдохновлено Паскалем):
RANGE_OR_INT
: ( INT ".." ) => INT { $setType(INT); }
| ( INT '.' ) => REAL { $setType(REAL); }
| INT { $setType(INT); }
;
На мой взгляд, это, по сути, положительное прогнозное утверждение в начале правила: если прогнозное совпадение совпадает INT ".."
тогда первое правило будет применяться (и соответствует INT
часть этого ввода), и так далее.
Я еще не нашел что-то подобное в ANTLR 4. Руководство по миграции с 2 по 3, похоже, не упоминает об этом, тогда как документ с 3 по 4 изменяет:
Самое большое различие между ANTLR 3 и 4 состоит в том, что ANTLR 4 принимает любую грамматику, которую вы ей даете, если только у грамматики не было косвенной рекурсии слева. Это означает, что нам не нужны синтаксические предикаты или обратный поиск, поэтому ANTLR 4 не поддерживает этот синтаксис; вы получите предупреждение за его использование.
Это соответствует сообщению об ошибке, которое я получаю, если я оставлю это по существу как есть:
(...)=> syntactic predicates are not supported in ANTLR 4
Хотя я могу понять, как более интеллектуальная реализация синтаксического анализатора могла бы разрешить эти неоднозначности, я не вижу, как это будет работать для лексеров.
Воспроизводящий пример
Чтобы быть уверенным, давайте попробуем это:
grammar Demo;
prog: atom (',' atom)* ;
atom: INT { System.out.println("INT: " + $INT.getText()); }
| REAL { System.out.println("REAL: " + $REAL.getText()); }
| a=INT RANGE b=INT { System.out.println("RANGE: " +
$a.getText() + " .. " + $b.getText()); }
;
WS : (' ' | '\t' | '\n' | '\r')+ -> skip ;
INT : ('0'..'9')+ ;
REAL: INT '.' INT? | '.' INT ;
RANGE: '..' ;
Сохранить это в Demo.g
, затем скомпилируйте и запустите:
$ wget -nc http://www.antlr.org/download/antlr-4.5.2-complete.jar
$ java -jar antlr-4.5.2-complete.jar Demo.g
$ javac -cp antlr-4.5.2-complete.jar Demo*.java
$ java -cp .:antlr-4.5.2-complete.jar org.antlr.v4.gui.TestRig \
Demo prog <<< '1,2.,3.4,5 ..6,7..8'
INT: 1
REAL: 2.
REAL: 3.4
RANGE: 5 .. 6
REAL: 7.
line 1:17 extraneous input '.8' expecting {<EOF>, ','}
Так что, похоже, я был прав: хотя удаление синтаксических предикатов могло бы быть уместным для синтаксического анализатора, лексер не сможет внезапно угадать правильный тип токена.
Основной вопрос
Так как же преобразовать этот конкретный пример в ANTLR 4? Есть ли способ выразить прогнозные условия? Или, возможно, способ иметь единственное правило, как INT '..'
испустить два разных токена?
Рекомендации и возможные решения
Глядя на грамматику ANTLR 4 Pascal, я замечаю, что она не позволяет действительным числам заканчиваться на .
без цифры после этого, поэтому изучение решения оттуда, кажется, не вариант.
Я видел семантические предикаты в ANTLR4? и синтаксические предикаты - Обновление с Antlr 3 до Antlr 4. Оба обсуждают синтаксические предикаты в правилах синтаксического анализа. У последнего также есть пример с правилами лексера, но упреждающий просмотр идентичен последующему правилу, что означает, что правила могут быть удалены без неблагоприятных последствий. Это не так в моем примере выше.
Ответы на проверку предыдущего / левого токена в лексере упоминают emit
метод лексера, с комментарием, ссылающимся на Как я могу создать более одного токена на правило лексера? Страница часто задаваемых вопросов в вики ANTLR 3, так что я думаю, что это один из подходов. Я превращу это в ответ, если никто не победит меня, и если я смогу заставить его работать в моем примере.
Ответ на отрицательный прогноз ANTLR4 в лексере использует _input.LA(int)
метод, чтобы изучить прогноз. Часто задаваемые вопросы о лексическом анализе ANTLR 4 _input.LA
не вдаваясь в детали. Это должно работать и для приведенного выше примера, но будет трудно для сценариев, где нужно рассмотреть более одного символа.
3 ответа
Вот очень короткое решение:
@lexer::members { private int _pos; }
INT_RANGE: INT { _pos=_input.index(); setType(INT); emit(); }
'..' { _input.seek(_pos); };
Это соответствует целому INT '..'
выражение, но затем перематывает ввод сразу после INT
где мы испускаем токен и сохраняем позицию. Эта позиция затем используется в конце правила, чтобы перемотать ввод более постоянным образом.
Однако существует проблема: полученные токены будут содержать неверную информацию о положении, поскольку _input.seek
не повлияет на то, что getCharPositionInLine
возвращается. В этом случае можно сделать
setCharPositionInLine(getCharPositionInLine() - 2)
в конце правила, но этот подход не будет работать, если вместо ..
один имел дело с вводом переменной длины. Я надеялся, что смогу сохранить результат getCharPositionInLine()
в первом действии, но, к сожалению, это уже отражает конец всего выражения.
Смотря на LexerATNSimulator.evaluatePredicate
Я вижу, что этот метод пытается восстановить заданное положение. Таким образом, мы можем получить правильное состояние, используя семантический предикат для его побочных эффектов:
@lexer::members {
private int _savedIndex, _savedLine, _savedColumn;
private boolean remember() {
_savedIndex = _input.index();
_savedLine = getLine();
_savedColumn = getCharPositionInLine();
return true;
}
private void recall(int type) {
_input.seek(_savedIndex);
setLine(_savedLine);
setCharPositionInLine(_savedColumn);
setType(type);
}
}
INT_RANGE: INT { remember() }? '..' { recall(INT); } ;
Имейте в виду, что семантический предикат будет выполняться в тот момент времени, когда еще не гарантируется, что все выражение будет фактически совпадать. Так что если вы используете этот трюк в нескольких местах, вы должны быть осторожны, чтобы не получить remember()
звонки по разным правилам переписывают состояние. Если вы сомневаетесь, вы можете использовать несколько таких функций или индекс в массиве, чтобы сделать каждое совпадение однозначным.
Источники тока (на момент написания статьи) Lexer
Реализация содержит несколько записей документации, касающихся выдачи нескольких токенов. Они, конечно, представлены в Lexer
API JavaDoc также. В соответствии с этим необходимо сделать следующее:
Override
emit(Token)
:По умолчанию не поддерживает несколько выбросов
nextToken
вызов по соображениям эффективности. Подкласс и переопределить этот метод,nextToken
, а такжеgetToken
(вставлять токены в список и извлекать из этого списка, а не одну переменную, как это делает эта реализация).Override
nextToken()
,Override
getToken()
:Переопределить, если испускается несколько токенов.
Обязательно установите
_token
не-null
:Если вы создаете подкласс для разрешения нескольких эмиссий токена, то установите для него значение, соответствующее последнему токену, или для чего-нибудь ненулевого, чтобы механизм автоматического генерирования токена не испускал другой токен.
Тем не менее, я не понимаю, почему переопределение getToken
было бы важно, так как я не вижу вызовов этого метода нигде в библиотеке времени выполнения. И если вы установите _token
тогда это будет вывод getToken
также.
Итак, что я сделал, чтобы испустить два токена из одного правила:
@lexer::members {
private Token _queued;
@Override public Token nextToken() {
if (_queued != null) {
emit(_queued);
_queued = null;
return getToken();
}
return super.nextToken();
}
@Override public Token emit() {
if (_type != INT_RANGE)
return super.emit();
Token t = _factory.create(
_tokenFactorySourcePair, INT, null, _channel,
_tokenStartCharIndex, getCharIndex()-3,
_tokenStartLine, _tokenStartCharPositionInLine);
_queued = _factory.create(
_tokenFactorySourcePair, RANGE, null, _channel,
getCharIndex()-2, getCharIndex()-1, _tokenStartLine,
_tokenStartCharPositionInLine + getCharIndex()-2 -
_tokenStartCharIndex);
emit(t);
return t;
}
}
INT_RANGE: INT '..' ;
Однако все вычисления позиции были довольно утомительными и дали мне другую (и, по крайней мере, для этого приложения, гораздо лучшую) идею, которую я опубликую в отдельном ответе.
Это еще одна попытка реализации опережающего просмотра.
В основном для потомков и проверки.
У меня есть язык, похожий на XML, но с менее строгими правилами.
Следующий пример:
<myTag>a bit of <text </myTag>
Должен быть токенизирован как:
< TagOpen
myTag TagName
> TagClose
a bit of <text Text
</ TagOpenClose
myTag TagName
> TagClose
Обратите внимание, как текст не мешает.
Для этого мне нужно заглянуть вперед и убедиться, что это<
является частью допустимого тега , а затем и допустимого элемента .
В следующем фрагменте обратите внимание, какisValidTag
используется функция. Я создаю новый экземпляр своего лексера, передавая тот же_input
поток, но восстанавливая свою позицию в конце метода.
@lexer::members {
private boolean isValidTag() {
final int mark = _input.mark();
final int index = _input.index();
final Lexer lexer = new MyCustomLexer(_input);
lexer.mode(TAG);
Token token = lexer.nextToken();
boolean isMatch = false;
if (token.getType() == TagName) {
token = lexer.nextToken();
if (token.getType() == TagClose ||
token.getType() == AttrName) {
isMatch = true;
}
}
_input.seek(index);
_input.release(mark);
return isMatch;
}
}
TagOpenClose : '</' -> mode(TAG);
TagOpen : '<' { isValidTag() }? -> mode(TAG);
Text : ~[<\n]+;
TextTagOpen : '<' -> more;
NewLine : ('\r\n' | '\r' | '\n');
...
Кажется, это работает прилично, хотя я не уверен в производительности и крайних случаях.