Programming Languages
June 17, 2022

Чиним grun в Antlr

Наткнулся на интересное поведение инструмента grun в Antlr. Для того, чтобы визуализация правильно заработала, потребовалось наложить дополнительные ограничения на грамматку.

Исходная грамматика

Для примера возьмем грамматику из примера самой библиотеки Antlr:

Hello.g4:

// Define a grammar called Hello
grammar Hello;
r  : 'hello' ID ;         // match keyword hello followed by an identifier
ID : [a-z]+ ;             // match lower-case identifiers
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
hello.txt

hello tale

Компилируем грамматику [1]:

java -cp ".:antlr-X.Y.Z-complete.jar" org.antlr.v4.Tool Hello.g4

Собираем полученные классы:

javac -cp antlr-X.Y.Z-complete.jar *.java

Запускаем grun:

java -cp ".;antlr-X.Y.Z-complete.jar" org.antlr.v4.gui.TestRig Hello r -gui hello.txt

Получаем вот такую картинку:

Очень удобно, мы видим структуру кода. Очень помогает при работе с грамматикой.

Делим грамматику на лексер и парсер

Использовать грамматику в таком виде можно, но только до тех пор, пока она небольшая. Крупную грамматику (эмпирически — больше экрана текста) нужно делить на две части.

HelloLexer.g4

lexer grammar HelloLexer;

ID : [a-z]+ ;             // match lower-case identifiers
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
HelloParser.g4

grammar HelloParser;

options {
    tokenVocab = HelloLexer;
}

r  : 'hello' ID ;         // match keyword hello followed by an identifier

Компилируем грамматику:

java -cp ".;antlr-X.Y.Z-complete.jar" org.antlr.v4.Tool HelloParser.g4  HelloLexer.g4

Компилируется без замечаний.

Снова компилируем Java-код:

javac -cp antlr-X.Y.Z-complete.jar *.java

Запускаем grun:

java -cp ".;antlr-X.Y.Z-complete.jar" org.antlr.v4.gui.TestRig HelloParser r -gui hello.txt

Обращаю внимание на то, что мы теперь вызываем grun для грамматики HelloParser, что отражено в команде вызова.

Получаем неверное дерево

и очень интересное сообщение об ошибке:

line 1:5 token recognition error at: ' '
line 1:6 token recognition error at: 't'
line 1:7 token recognition error at: 'a'
line 1:8 token recognition error at: 'l'
line 1:9 token recognition error at: 'e'
line 1:10 token recognition error at: '\r'
line 1:11 token recognition error at: '\n'
line 2:0 missing 'hello' at '<EOF>'
Ничего не понятно, но очень интересно ©.

Попробуем изменить тактику.

Разбирательство

Переименуем файл HelloParser.g4 в Hello.g4, меняя, соответственно, название грамматики:

Hello.g4
grammar Hello;

options {
    tokenVocab = HelloLexer;
}

r  : 'hello' ID ;         // match keyword hello followed by an identifier

Компилируем:

java -cp ".;antlr-X.Y.Z-complete.jar" org.antlr.v4.Tool Hello.g4 HelloLexer.g4

Получаем довольно странное сообщение об ошибке:

warning(125): Hello.g4:7:13: implicit definition of token ID in parser

ID у нас определен в лексере, по идее, не должно быть здесь ошибки.

Поиск по интернету дал наводку, что такое предупреждение выдается для лексем, определенных в парсере, в нашем случае это 'hello', т.е. похоже, что в Antlr здесь ошибка, можно поразбираться.

Меняем 'hello' на HELLO.

HelloLexer.g4

lexer grammar HelloLexer;

HELLO : 'hello';
ID : [a-z]+ ;             // match lower-case identifiers
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
HelloParser.g4

grammar HelloParser;

options {
    tokenVocab = HelloLexer;
}

r  : hello ID ;         // match keyword hello followed by an identifier
hello : HELLO;

Компилируем:

java -cp ".;antlr-X.Y.Z-complete.jar" org.antlr.v4.Tool HelloLexer.g4 HelloParser.g4
javac -cp antlr-X.Y.Z-complete.jar *.java

Запускаем grun:

java -cp ".;antlr-X.Y.Z-complete.jar" org.antlr.v4.gui.TestRig HelloParser r -gui hello.txt

Ошибка:

Can't load HelloParser as lexer or parser

Пробуем подменить название грамматики:

java -cp ".;antlr-X.Y.Z-complete.jar" org.antlr.v4.gui.TestRig Hello r -gui hello.txt

Ошибка еще интереснее:

Exception in thread "main" java.lang.ClassNotFoundException: HelloParser
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:602)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
        at org.antlr.v4.gui.TestRig.process(TestRig.java:150)
        at org.antlr.v4.gui.TestRig.main(TestRig.java:119)

Решение

Решение оказалось в том, что при разделении на лексер и парсер необходимо, чтобы прасерная грамматика была помечена как parser:

HelloLexer.g4

lexer grammar HelloLexer;

HELLO : 'hello';
ID : [a-z]+ ;             // match lower-case identifiers
WS : [ \t\r\n]+ -> skip ; // skip spaces, tabs, newlines
HelloParser.g4

// см ключевое слово parser
parser grammar HelloParser;

options {
    tokenVocab = HelloLexer;
}

r  : hello ID ;         // match keyword hello followed by an identifier
hello : HELLO;

Вот при таком раскладе у нас работает все, что нужно.

Компилируем, запускаем grun:

java -cp ".;antlr-X.Y.Z-complete.jar" org.antlr.v4.gui.TestRig Hello r -gui hello.txt

Обращаю внимание, что, несмотря на названия HelloLexer и HelloParser, grun мы вызываем для грамматики Hello. Lexer и Parser будут подставлены самостоятельно.

Выводы

  1. При разделении грамматики нужно лексемы переносить в лексер.
  2. Грамматики нужно помечать как lexer/parser.
  3. Экспериментируйте :)

[1] Используем полный путь до antlr-X.Y.Z-complete.jar, на Windows используем разделитель ;.