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