Статьи

О необходимости общей библиотеки вокруг ANTLR: использование отражения для построения метамодели

Я инженер по языку: я использую несколько инструментов для определения и обработки языков. Среди других инструментов я использую ANTLR: он прост, он гибок, я могу строить вещи вокруг него.

Однако я обнаружил, что собираю подобные инструменты вокруг ANTLR для разных проектов. Я вижу две проблемы с этим:

  • ANTLR — очень хороший строительный блок, но с одним только ANTLR мало что можно сделать: ценность заключается в обработке, которую мы можем выполнять в AST, и я не вижу экосистемы библиотек вокруг ANTLR
  • ANTLR не производит метамодель грамматики: без нее становится очень трудно создавать общие инструменты вокруг ANTLR

Позвольте мне объяснить, что:

  • Для людей с опытом работы с EMF: нам в основном нужен Ecore-эквивалент для каждой грамматики.
  • Для остальных: прочитайте следующий абзац

Зачем нам нужна метамодель

Предположим, я хочу создать универсальную библиотеку для создания файла XML или документа JSON из AST, созданного ANTLR. Как я мог это сделать?

Ну, учитывая ParseRuleContext, я могу взять индекс правила и найти имя. Я сгенерировал синтаксический анализатор для грамматики Python, чтобы иметь несколько примеров, поэтому давайте посмотрим, как это сделать с реальным классом:

1
2
Python3Parser.Single_inputContext astRoot = pythonParse(...my code...);
String ruleName = Python3Parser.ruleNames[astRoot.getRuleIndex()];

Давайте посмотрим на класс Single_inputContext:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public static class Single_inputContext extends ParserRuleContext {
    public TerminalNode NEWLINE() { return getToken(Python3Parser.NEWLINE, 0); }
    public Simple_stmtContext simple_stmt() {
        return getRuleContext(Simple_stmtContext.class,0);
    }
    public Compound_stmtContext compound_stmt() {
        return getRuleContext(Compound_stmtContext.class,0);
    }
    public Single_inputContext(ParserRuleContext parent, int invokingState) {
        super(parent, invokingState);
    }
    @Override public int getRuleIndex() { return RULE_single_input; }
    @Override
    public void enterRule(ParseTreeListener listener) {
        if ( listener instanceof Python3Listener ) ((Python3Listener)listener).enterSingle_input(this);
    }
    @Override
    public void exitRule(ParseTreeListener listener) {
        if ( listener instanceof Python3Listener ) ((Python3Listener)listener).exitSingle_input(this);
    }
}

Я должен получить что-то вроде этого:

1
2
3
4
<Single_input NEWLINES="...">
   <Simple_stmt>...</Simple_stmt>
   <Compund_stmt>...</Compunt_stmt>
</root>

Хороший. Мне очень легко взглянуть на класс и распознать эти элементы, однако как я могу сделать это автоматически?

Отражение, очевидно, подумаешь.

Да. Это будет работать. Однако что, если, когда у нас есть несколько элементов? Возьми этот класс:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static class File_inputContext extends ParserRuleContext {
    public TerminalNode EOF() { return getToken(Python3Parser.EOF, 0); }
    public List NEWLINE() { return getTokens(Python3Parser.NEWLINE); }
    public TerminalNode NEWLINE(int i) {
        return getToken(Python3Parser.NEWLINE, i);
    }
    public List stmt() {
        return getRuleContexts(StmtContext.class);
    }
    public StmtContext stmt(int i) {
        return getRuleContext(StmtContext.class,i);
    }
    public File_inputContext(ParserRuleContext parent, int invokingState) {
        super(parent, invokingState);
    }
    @Override public int getRuleIndex() { return RULE_file_input; }
    @Override
    public void enterRule(ParseTreeListener listener) {
        if ( listener instanceof Python3Listener ) ((Python3Listener)listener).enterFile_input(this);
    }
    @Override
    public void exitRule(ParseTreeListener listener) {
        if ( listener instanceof Python3Listener ) ((Python3Listener)listener).exitFile_input(this);
    }
}

Теперь методы NEWLINE и stmt возвращают списки. Вы можете помнить, что в общем случае дженерики не так хорошо работают с отражением в Java. В этом случае нам повезло, потому что есть решение:

1
2
3
4
5
6
7
Class clazz = Python3Parser.File_inputContext.class;
Method method = clazz.getMethod("stmt");
Type listType = method.getGenericReturnType();
if (listType instanceof ParameterizedType) {
    Type elementType = ((ParameterizedType) listType).getActualTypeArguments()[0];
    System.out.println("ELEMENT TYPE "+elementType);
}

Это напечатает:

Класс ТИПА ЭЛЕМЕНТА me.tomassetti.antlrplus.python.Python3Parser $ StmtContext

Таким образом, мы можем охватить также дженерики. Хорошо, использование отражения не является идеальным, но мы можем извлечь некоторую информацию оттуда.

Я не уверен на 100%, что этого будет достаточно, но мы можем начать.

Как должна понравиться метамодель?

Чтобы определить метамодели, я бы не стал придумывать что-нибудь причудливое. Я бы использовал классическую схему, которая лежит в основе EMF, и она похожа на ту, что доступна в MPS.

Я бы добавил контейнер с именем Package или Metamodel . Пакет будет перечислять несколько сущностей. Мы также можем пометить одну из этих сущностей как корневую сущность.

Каждый субъект будет иметь:

  • имя
  • необязательный родительский объект (от которого он наследует свойства и отношения)
  • список свойств
  • список отношений

Каждое свойство будет иметь:

  • имя
  • тип, выбранный среди примитивного типа. На практике я рассчитываю использовать только строки и целые числа. Возможно перечисление в будущем
  • кратность (1 или много)

Каждое отношение будет иметь:

  • имя
  • вид: содержание или ссылка . Теперь AST знает только о контейнерах , однако позже мы сможем реализовать преобразование символов и преобразование моделей, и на этом этапе нам понадобятся ссылки
  • тип цели: другая сущность
  • кратность (1 или много)

Следующие шаги

Я бы начал создавать метамодель, а затем создавать универсальные инструменты, используя преимущества метамодели.

Есть и другие вещи, которые обычно нужны:

  • преобразования: AST, который я обычно получаю из ANTLR, определяется тем, как я вынужден выражать грамматику для получения чего-то доступного для анализа. Иногда я также должен сделать некоторый рефакторинг, чтобы улучшить производительность. Я хочу преобразовать AST после разбора, чтобы приблизиться к логической структуре языка.
  • unmarshalling: из AST я хочу произвести тест обратно
  • разрешение символов: это может быть совершенно не тривиально, поскольку я обнаружил, что создание решателя символов для Java

Да, я знаю, что некоторые из вас думают: просто используйте Xtext . Хотя мне нравится EMF (Xtext построен поверх него), у него крутая кривая обучения, и я видел многих людей, смущенных этим. Мне также не нравится, как OSGi играет с миром не-OSGi. Наконец, Xtext поставляется с большим количеством зависимостей.

Не поймите меня неправильно: я думаю, что Xtext — это удивительное решение во многих контекстах. Однако есть клиенты, которые предпочитают более гибкий подход. Для случаев, в которых это имеет смысл, нам нужна альтернатива. Я думаю, что он может быть построен поверх ANTLR, но есть над чем поработать.

Кстати, много лет назад я создал нечто подобное для .NET и назвал это NetModelingFramework .