Эта проблема может быть решена с помощью лексических режимов, если один режим для внутренней части строк и один (или более) для внешней.Наблюдение "
снаружи переключится на внутренний режим, а наблюдение \(
или "
переключится обратно наружу.Единственной сложной частью было бы видеть )
снаружи: иногда он должен переключаться обратно внутрь (потому что он соответствует \(
), а иногда не должен (когда он соответствует простой (
)).
Самый простой способ добиться этого будет выглядеть так:
Lexer:
lexer grammar StringLexer;
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]* ;
DQUOTE: '"' -> pushMode(IN_STRING);
LPAR: '(' -> pushMode(DEFAULT_MODE);
RPAR: ')' -> popMode;
mode IN_STRING;
TEXT: ~[\\"]+ ;
BACKSLASH_PAREN: '\\(' -> pushMode(DEFAULT_MODE);
ESCAPE_SEQUENCE: '\\' . ;
DQUOTE_IN_STRING: '"' -> type(DQUOTE), popMode;
Parser:
parser grammar StringParser;
options {
tokenVocab = 'StringLexer';
}
start: exp EOF ;
exp : '(' exp ')'
| IDENTIFIER
| DQUOTE stringContents* DQUOTE
;
stringContents : TEXT
| ESCAPE_SEQUENCE
| '\\(' exp ')'
;
Здесь мынажимайте режим по умолчанию каждый раз, когда мы видим (
или \(
, и нажимайте на режим каждый раз, когда мы видим )
.Таким образом, он будет возвращаться внутрь строки только в том случае, если режим на вершине стека является строковым режимом, что будет иметь место только в том случае, если с момента последнего \(
.
не останется незамеченных
(
.
Этот подход работает, но имеет недостаток в том, что несогласованный )
вызовет исключение пустого стека, а не нормальную синтаксическую ошибку, потому что мы вызываем popMode
для пустого стека.
Чтобы избежать этого, мы можем добавить элемент, который отслеживает, насколько глубоко мы вложены в скобки, и не выталкивает стек, когда уровень вложенности равен 0 (т. Е. Если стек пуст):
@members {
int nesting = 0;
}
LPAR: '(' {
nesting++;
pushMode(DEFAULT_MODE);
};
RPAR: ')' {
if (nesting > 0) {
nesting--;
popMode();
}
};
mode IN_STRING;
BACKSLASH_PAREN: '\\(' {
nesting++;
pushMode(DEFAULT_MODE);
};
(Части, которые я пропустил, такие же, как в предыдущей версии).
Это работает и генерирует нормальные синтаксические ошибки для несопоставленных )
с.Тем не менее, он содержит действия и, следовательно, больше не зависит от языка, что является проблемой, только если вы планируете использовать грамматику из нескольких языков (и в зависимости от языка вам может даже повезти, и код может быть действительным во всехцелевые языки).
Если вы хотите избежать действий, последняя альтернатива будет иметь три режима: один для кода, который находится вне каких-либо строк, один для внутренней части строки и один длявнутри \()
.Третий будет почти идентичен внешнему, за исключением того, что он будет толкать и выдвигать режим при отображении скобок, тогда как внешний - нет.Чтобы оба режима производили одинаковые типы токенов, все правила третьего режима будут вызывать type()
.Это будет выглядеть следующим образом:
lexer grammar StringLexer;
IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]* ;
DQUOTE: '"' -> pushMode(IN_STRING);
LPAR: '(';
RPAR: ')';
mode IN_STRING;
TEXT: ~[\\"]+ ;
BACKSLASH_PAREN: '\\(' -> pushMode(EMBEDDED);
ESCAPE_SEQUENCE: '\\' . ;
DQUOTE_IN_STRING: '"' -> type(DQUOTE), popMode;
mode EMBEDDED;
E_IDENTIFIER: [a-zA-Z_][a-zA-Z0-9_]* -> type(IDENTIFIER);
E_DQUOTE: '"' -> pushMode(IN_STRING), type(DQUOTE);
E_LPAR: '(' -> type(LPAR), pushMode(EMBEDDED);
E_RPAR: ')' -> type(RPAR), popMode;
Обратите внимание, что теперь мы больше не можем использовать строковые литералы в грамматике синтаксического анализатора, поскольку строковые литералы нельзя использовать, когда несколько правил лексера определены с использованием одного и того же строкового литерала.Так что теперь мы должны использовать LPAR
вместо '('
в парсере и т. Д. (Мы уже должны были сделать это для DQUOTE
по той же причине).
Поскольку эта версия включает в себя многодублирование (особенно при увеличении количества токенов) и предотвращение использования строковых литералов в грамматике синтаксического анализатора. Обычно я предпочитаю версию с действиями.
Полный код для всех трех альтернатив также может бытьнайдено на GitHub .