Я инженер по языку: я использую несколько инструментов для определения и обработки языков. Среди других инструментов я использую 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 .