JavaCC: токены в зависимости от контекста
JavaCC позволяет нам работать с набором токенов. Но часто бывает нужно сделать так, чтобы в некотором месте лексер не использовал набор основных токенов, а работал бы иначе. А потом снова возвращался к прежнему состоянию. Например, это может понадобиться для обработки многострочных комментариев – после встречи токена начала такого комментария лексер должен переключиться на режим, в котором бы игнорировалось всё, кроме токена его окончания. В случае комментариев это можно сделать стандартным для JavaCC способом – в настройке токенайзера:
SKIP : { // Однострочный комментарий < "//" (~["\r", "\n"])* > // Начало многострочного комментария - переход к другому состоянию лексера | < "/*" > : ML_COMMENT_STATE } // В этом состоянии есть существуют только два токена: конец комментария // и всё остальное. По нахождении токена конца комментария состояние возвращается в DEFAULT <ML_COMMENT_STATE> SKIP : { < "*/" > : DEFAULT | < ~[] > } |
А иногда бывает так, что перейти к другому набору токенов нужно не в токенайзере, а именно в парсере. Например, когда парсер встретил специальный токен и потом идут данные в другом формате. При парсинге различных DSL это может встречаться, например, в следующем варианте. Допустим у нас есть токен t_identifier, который начинается с буквы и далее идут буквы или цифры с подчёркиваниями. То есть – обычный идентификатор. Но мы хотим добавить поддержку директивы, синтаксис которой содержит ключевое слово, которое является валидным идентификатором. Например, “ignore”. Если не добавить специальный токен для этой сигнатуры, лексер при встрече с ним выплюнет нам токен-идентификатор. А если добавить токен
SKIP : { " " | "\t" | "\n" | "\r" } TOKEN: { <t_cat: "category"> | < tt_identifier: <LETTER> ( <LETTER>|<DIGIT> )* > // Строка в кавычках | < tt_string: "\"" (~["\"","\\","\n","\r"] | "\\" (["n","t","b","r","f","\\","\'","\""] | ["0"-"7"] (["0"-"7"])? | ["0"-"3"] ["0"-"7"] ["0"-"7"]))* "\"" > | < #LETTER: [ "_", "a"-"z", "A"-"Z", "а"-"я", "А"-"Я" ] > | < #DIGIT: [ "0"-"9"] > } // Да, для каждого лексического состояния нужно определить не только TOKEN, но и SKIP // и всё остальное, что используется <MYSTATE> SKIP : { " " | "\t" | "\n" | "\r" } <MYSTATE> TOKEN: { < tt_mystate_string: "\"" (~["\"","\\","\n","\r"] | "\\" (["n","t","b","r","f","\\","\'","\""] | ["0"-"7"] (["0"-"7"])? | ["0"-"3"] ["0"-"7"] ["0"-"7"]))* "\"" > | <tt_mystate_ignore: "ignore" > | <tt_mystate_semicolon: ";"> } // Часть того самого хака, необходимая для работы функции SetState(). TOKEN_MGR_DECLS : { void backup(int n) { input_stream.backup(n); } } // Функция SetState() необходима для безопасного перехода к другому лексическому состоянию // (так как токенайзер конвейеризирует поток токенов, и нужно этот конвейер сбросить). JAVACODE private void SetState(int state) { if (state != token_source.curLexState) { Token root = new Token(), last=root; root.next = null; // First, we build a list of tokens to push back, in backwards order while (token.next != null) { Token t = token; // Find the token whose token.next is the last in the chain while (t.next != null && t.next.next != null) t = t.next; // put it at the end of the new chain last.next = t.next; last = t.next; // If there are special tokens, these go before the regular tokens, // so we want to push them back onto the input stream in the order // we find them along the specialToken chain. if (t.next.specialToken != null) { Token tt=t.next.specialToken; while (tt != null) { last.next = tt; last = tt; tt.next = null; tt = tt.specialToken; } } t.next = null; }; while (root.next != null) { token_source.backup(root.next.image.length()); root.next = root.next.next; } jj_ntk = -1; token_source.SwitchTo(state); } } // Далее уже идёт наш код парсера private void Category() : { Token tname; } { <t_cat> <tt_string> ( "my state" MyState() | ";" ) } private void MyState() : { int entryState = token_source.curLexState; } { { SetState(MYSTATE); } <tt_mystate_string> [<tt_mystate_ignore> <tt_mystate_string>] <tt_mystate_semicolon> { SetState(entryState); } } |
Неудобство заключается в том, что в изменённом (не-DEFAULT) состоянии нельзя использовать строковые литералы просто так, не добавляя для них токены (то есть вместо “;” мы должны записать <tt_mystate_semicolon>). Возможно, это баг, но может оказаться и специальным ограничением. Ветку с обсуждением этой проблемы можно прочитать здесь. Впрочем, несмотря на эту проблему, у нас всё получилось, и мы вернули себе полный контроль над процессом.
0