JavaCC: токены в зависимости от контекста

Written by elwood

JavaCC позволяет нам работать с набором токенов. Но часто бывает нужно сделать так, чтобы в некотором месте лексер не использовал набор основных токенов, а работал бы иначе. А потом снова возвращался к прежнему состоянию. Например, это может понадобиться для обработки многострочных комментариев – после встречи токена начала такого комментария лексер должен переключиться на режим, в котором бы игнорировалось всё, кроме токена его окончания. В случае комментариев это можно сделать стандартным для JavaCC способом – в настройке токенайзера:

SKIP :
{
  // Однострочный комментарий
  < "//" (~["\r", "\n"])* >
  // Начало многострочного комментария - переход к другому состоянию лексера
| < "/*" > : ML_COMMENT_STATE
}
 
// В этом состоянии есть существуют только два токена: конец комментария
// и всё остальное. По нахождении токена конца комментария состояние возвращается в DEFAULT
<ML_COMMENT_STATE> SKIP :
{
  < "*/" > : DEFAULT
| < ~[] >   
}

А иногда бывает так, что перейти к другому набору токенов нужно не в токенайзере, а именно в парсере. Например, когда парсер встретил специальный токен и потом идут данные в другом формате. При парсинге различных DSL это может встречаться, например, в следующем варианте. Допустим у нас есть токен t_identifier, который начинается с буквы и далее идут буквы или цифры с подчёркиваниями. То есть – обычный идентификатор. Но мы хотим добавить поддержку директивы, синтаксис которой содержит ключевое слово, которое является валидным идентификатором. Например, “ignore”. Если не добавить специальный токен для этой сигнатуры, лексер при встрече с ним выплюнет нам токен-идентификатор. А если добавить токен , то мы резервируем это слово, делая его недоступным в других контекстах, там, где слово ignore могло бы быть именем переменной. В JavaCC FAQ написано о том, что можно изменить лексическое состояние из кода парсера, но в этом случае можно огрести проблем с конвейером токенайзера. Но нашелся хитрый мужик, который запилил для этого удобный костыль (его же и приводят в этом FAQ в качестве безопасного способа). Итак, пишем код:

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>). Возможно, это баг, но может оказаться и специальным ограничением. Ветку с обсуждением этой проблемы можно прочитать здесь. Впрочем, несмотря на эту проблему, у нас всё получилось, и мы вернули себе полный контроль над процессом.