ООО «ЦИТ» corporate blog
Website development
Java
Eclipse
Compilers
November 2015 18

Введение в разработку предметно-ориентированных языков (DSL) с помощью EMFText

Tutorial

Это 5-я статья цикла по разработке, управляемой моделями. В предыдущих статьях мы уже разобрались с метамоделями, валидацией моделей, некоторыми нотациями для моделей (диаграммы и таблицы). Всё это было в рамках пространства моделирования MOF. Сегодня мы построим мост в пространство моделирования EBNF – познакомимся с текстовой нотацией для MOF-моделей.

Введение


Вообще на тему разработки языков программирования общего назначения и предметно-ориентированных языков очень много информации. Каждый, кто этим интересовался, наверняка имеет общее представление о лексерах, парсерах, синтаксических деревьях и т.п. Но мы подойдем к этому немного с другой стороны. Мы не будем рассматривать разработку DSL вообще, нас она интересует только с точки зрения модельно-ориентированной разработки.



Примечание

Честно говоря, введение получилось какое-то мозговыносное. Оно ориентировано в основном на специалистов по разработке, управляемой моделями. Можно его пролистать.

На рисунке красным, желтым, фиолетовым и зеленым цветами обозначено то, что обычно рассматривается в теории языков программирования. На языке EBNF (или каком-то другом) разрабатывается грамматика языка. Затем программисты пишут исходный код в соответствии с грамматикой. Исходный код скармливается парсеру, который преобразует текст программы в некоторое внутреннее представление (назовём его абстрактный семантический граф). Затем этот граф используется интерпретатором, компилятором, редактором или кодогенератором.

Это очень упрощенная и схематичная классика теории языков программирования. Мы не будем слишком подробно её рассматривать.

Параллельно со всем этим есть другая область – модельно-ориентированная разработка, которой посвящен данный цикл статей.

В модельно-ориентированной разработке совершенно всё от мыслей в голове разработчика до исходного кода, модульных тестов или документации рассматривается как модель (это основная, первичная сущность). А процесс разработки – это преобразование одних моделей в другие.

  1. Например, сначала в сознании разработчика возникает некий образ (модель) будущей программы.
  2. Затем он преобразует этот мысленный образ в UML-модель.
  3. На основе UML-модели пишет исходный код (тоже модель с точки зрения модельно-ориентированной разработки).
  4. Для исходного кода пишет модульные тесты (и это модель).
  5. Пишет документацию (всё – модель).

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

Некоторые модели очень похожи друг на друга. Например, все UML-модели строятся по определенным правилам в единой нотации (у них общая метамодель – UML). Какие-нибудь BPMN- или ER-модели уже отличаются от UML. Но, тем не менее, они гораздо ближе к UML чем исходный код или мысли программиста.

Это связано с тем, что UML, BPMN и ER – это метамодели, построенные на основе одной метаметамодели MOF. А грамматика (метамодель) языка программирования построена на другой метаметамодели – EBNF. Мысли программиста соответствуют тоже некоторой метамодели, которая соответствует некоторой метаметамодели, которая на данный момент совершенно неформализуемая.

Каждая метаметамодель образует своё пространство моделирования, которое достаточно сильно отличается от других.

Примечание

Если вы не понимаете о чём я толкую, то можете прочитать первую статью цикла про OCL и метамодели. А также статью про пространства моделирования.

Если необходимо преобразовывать модели внутри одного пространства моделирования (например, MOF), то это элементарно. Есть соответствующие спецификации и инструменты, к которым мы вернёмся в следующей статье.

Но если необходимо а) преобразовать исходный код в UML или б) из BPMN-моделей формировать модульные тесты или документацию, то это сделать уже несколько сложнее. Для этого нам нужен мост между двумя пространствами моделирования. И тут к нам на помощь приходит теория языков программирования.

В данной статье мы рассмотрим метамодель одного очень простого предметно-ориентированного языка (синий блок на рисунке), опишем его грамматику (зеленый блок на рисунке). А также сгенерируем (в модельно-ориентированной разработке вообще не очень принято писать код вручную парсер и кодогенератор – мост между пространствами моделирования MOF и EBNF. Также сгенерируем редактор этого языка, в будущем он нам не понадобится, но пусть будет.

Для Eclipse Modeling Framework есть несколько инструментов, которые могут нам с этим помочь.

MOF Model to Text Transformation Language (Acceleo)

Это язык шаблонов для генерации текста из MOF-моделей, описанный в спецификации OMG. Acceleo – это реализация спецификации OMG. Спецификация не обновлялась с 2008 года, однако, Acceleo успешно используется во многих проектах. Язык очень простой, может быть и не нужно в нём ничего обновлять. Мы рассмотрим его в одной из следующих статей более подробно.

Плюс этого языка в том, что он позволяет достаточно легко формировать из моделей текст. Если нужно по-быстрому сформировать из UML- или ER-модели SQL-запросы или сделать выгрузку из модели в CSV-формате, то этот язык оптимален.

Основной недостаток заключается в том, что этот мост односторонний. Он не позволяет распарсить текст и превратить его обратно в модель. Также приходится уделять много внимания форматированию шаблонов (пробелам, переводам строк), чтобы результирующий текст был правильно отформатирован. При этом сами шаблоны становятся не очень читаемыми.

Кстати, Acceleo – это фактически шаблонная надстройка над OCL. Если вы читали эту статью, то для освоения Acceleo вам остаётся узнать ещё несколько конструкций.

EMFText

EMFText – это уже гораздо более интересная штука, чем Acceleo. Вы описываете метамодель и синтаксис языка. В итоге получаете двунаправленный мост между пространствами моделирования MOF и EBNF. Для вас автоматически формируется парсер (из текста в модель), кодогенератор (из модели в текст), а также редактор (с подсветкой синтаксиса и автодополнением) и заготовки для компилятора, интерпретатора и отладчика языка.

Тут есть примеры реализованных с помощью EMFText языков.

В данной статье мы будем использовать EMFText.

Xtext

Xtext по функциональности аналогичен EMFText. Отличается более активным коммьюнити. И подходом к генерации парсера и кодогенератора, которые зависят от runtime-библиотек Xtext. В отличие от EMFText, который нужен только в design-time и не нужен в runtime. По этой причине в наших проектах мы используем именно EMFText, а не Xtext.

Epsilon Generation Language

Аналог Acceleo для Epsilon.

Human Usable Textual Notation

Также стоит отметить OMG HUTN. Это текстовый синтаксис для сериализации MOF-моделей. Можете воспринимать его как JSON для MOF-моделей. Для Epsilon существует его реализация. Однако, нам эта штука не подходит, потому что нам потребуется описывать произвольный синтаксис, а не только с фигурными скобками.

Немного теории


Прежде чем перейти к практике всё-таки потребуется немного теории.

Примечание

Этот раздел не претендует ни на полноту охвата, ни на точность. Всё описывается очень упрощенно и схематично, только чтобы были понятны последующие разделы. Если вас интересует теория языков программирования, то лучше обратиться к источникам, которые посвящены этой теме.

Мы строим мост между пространствами моделирования EBNF и MOF. С одной стороны моста исходный код, с другой – некая модель программы (для определенности будем называть её абстрактный семантический граф).

Обычно абстрактный семантический граф скрыт от программиста. Как именно он устроен – вопрос реализации компилятора или интерпретатора. Программисты с этим графом напрямую не работают, они работают только с исходным кодом.

В модельно-ориентированной разработке абстрактный семантический граф, наоборот, играет ключевую роль. Это уже не какая-то техническая внутренняя структура парсера, а модель с которой будет работать программист. Для программиста важно, как именно эта модель устроена, на сколько она удобна.

Примечание

Конечно, когда программисты пользуются рефлексивными возможностями языка, они работают именно с моделью программы, а не исходным кодом. Но при этом они, наверное, сами не понимая этого, попадают в область модельно-ориентированной разработки. Они рассматривают программу как модель.

На рисунке схематично показано как работают парсер и кодогенератор языка.



Лексический анализ

Сначала производится лексический анализ исходного кода, в результате которого текст разбивается на последовательность токенов. Обычно лексер выделяет токены с помощью контекстно-независимых регулярных выражений. Именно по этой причине в языках

  • существуют зарезервированные слова, которые нельзя использовать в идентификаторах, чтобы лексер мог отличить ключевое слово от идентификатора;
  • текстовые литералы заключаются в кавычки, чтобы лексер мог отличить их от зарезервированных слов или идентификаторов;
  • идентификаторы не могут начинаться или полностью состоять из цифр, иначе лексеру было бы сложно понять идентификатор это или числовой литерал.

Т.е. регулярные выражения для разных видов токенов по возможности не должны пересекаться. К сожалению, иногда они всё-таки пересекаются. Иногда это не проблема. А иногда это приводит к усложнению грамматики языка – мы столкнёмся с такой ситуацией в следующей статье про парсер SQL.

Синтаксический анализ

Затем производится синтаксический анализ последовательности токенов. Парсер, глядя в грамматику языка, упорядочивает токены в конкретное синтаксическое дерево. По структуре это дерево идентично EBNF-грамматике языка:

  • для начального нетерминального символа строится корневой узел дерева,
  • для других нетерминальных символов строятся внутренние узлы дерева,
  • для терминальных символов (токенов, лексем) строятся листовые узлы дерева.

Упрощение конкретного синтаксического дерева

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

В одной из следующих статей мы, вероятно, рассмотрим язык для арифметических выражений. Вы увидите, что конкретное синтаксическое дерево для такого языка напоминает Пизанскую башню.

Для простых языков достаточно удалить лишние промежуточные этажи башни. Для более сложных языков требуются более сложные упрощения. В итоге мы получаем абстрактное синтаксическое дерево.

У нас каждый узел дерева будет объектом определенного класса. Хотя, вообще, это не обязательно, мы вполне могли бы обойтись без классов и объектов, представив дерево, например, в виде XML-документа. Но нам нужна именно объектная модель, потому что MOF, к которому мы движемся объектный. Если бы мы строили мост к какому-то пространству моделирования отличному от MOF, то нам была бы нужна не объектная модель программы, а какая-то другая.

Семантический анализ

Очевидно, что программу, написанную на относительно сложном языке, мы не сможем представить в виде дерева. Например, если этот язык позволяет объявлять переменные, классы, типы, функции, а потом ссылаться на них, то при разрешении таких текстовых ссылок мы получаем абстрактный семантический граф.

Кодогенерация

Кодогенерация – это обратный парсингу процесс, когда из некоторого абстрактного представления программы (например, в виде абстрактного семантического графа) формируется текстовое представление программы.

Есть два подхода к кодогенерации: шаблоны и универсальный кодогенератор.

При использовании шаблонов пишется примерный текст будущей программы. Например, имена классов, переменных, функций в этом тексте заменены на специальные последовательности символов, вместо которых впоследствии подставляются фактические имена. Очевидно, что произвольный код с помощью шаблонов не сгенерируешь.

Универсальный кодогенератор принимает на вход некоторую модель программы (например, абстрактный семантический граф), а на выходе выдаёт соответствующий исходный код. Таким образом можно сгенерить какой угодно код. Однако реализовать универсальный кодогенератор гораздо сложнее, чем шаблон. Также могут возникнуть сложности с форматированием результирующего кода. Нужны либо дополнительные аннотации в модели, содержащие информацию о пробелах, переводах строк и т.п. Либо нужен форматировщик кода, который тоже нужно писать или где-то брать. В варианте с шаблонами это не нужно, вы прямо в шаблоне форматируете всё как надо.

К счастью, EMFText автоматически генерирует кодогенератор с простейшими возможностями форматирования кода.

Настройка


Как обычно, понадобится Eclipse Modeling Tools. Установите последнюю версию EMFText отсюда http://emftext.org/update_trunk.

Создание проекта


В отличие от предыдущих статей готового проекта нет. Да, он и не нужен, воспользуемся проектом, который создаётся по умолчанию (File -> New -> Other… -> EMFText Project).



В папке metamodel вы увидите заготовки для метамодели языка (myDSL.ecore) и его грамматики (myDSL.cs). Два этих файла полностью описывают язык. Почти всё остальное генерируется из них.



В данной статье мы ограничимся этим простым демонстрационным DSL.

Метамодель языка


Метамодель – это то о чём язык. Например, метамодель языка Java будет содержать метаклассы: класс, метод, переменная, выражение и т.д. Вы не можете описать на языке что-то, чего нет в его метамодели. Например, в метамодели Java 7 нет лямбда-выражений. Поэтому они недопустимы в коде, который пишется под Java 7.

На следующем рисунке изображена метамодель демонстрационного предметно-ориентированного языка, которую сгенерировал для нас EMFText.


Примечание

Если вы не понимаете, что изображено на рисунке, то можете прочитать статью про Eclipse Modeling Framework.

Наш предметно-ориентированный язык позволяет описывать некоторую модель сущностей (EntityModel), которая состоит из типов (Type) двух видов: сущности (Entity) и типы данных (DataType). Сущности могут быть абстрактными (abstract). У сущностей могут быть свойства (Feature) трёх видов (FeatureKind): атрибуты (attribute), ссылки (reference) и составные части (containment). Свойства очень простые, у них нет даже множественности.

По идее, атрибуты должны ссылаться только на типы данных. А ссылки и составные части должны ссылаться только на сущности. Но в данной метамодели на структурном уровне это никак не ограничивается. Вы вполне можете сделать тип данных составной частью некоторой сущности или можете в качестве типа атрибута указать сущность вместо типа данных. Что, наверное, не очень правильно. Исправить это можно двумя способами: 1) на структурном уровне или 2) с помощью дополнительных ограничений.

В первом случае для каждого вида свойств создаётся отдельный метакласс (именно так реализована сама метаметамодель Ecore). Т.е. удаляем перечисление FeatureKind, удаляем ассоциацию type, метакласс Feature делаем абстрактным и наследуем от него три метакласса: Attribute, Reference и Containment. Первому добавляем ссылку на DataType, а второму и третьему – на Entity.

Второй способ описан в статье про OCL.

Мы не будем исправлять этот недочёт. Более того, далее он даже поможет нам разобраться с механизмом разрешения ссылок.

Запуск редактора языка


Итак, мы более-менее разобрались с метамоделью демонстрационного предметно-ориентированного языка, который сгенерировал для нас EMFText. Прежде чем перейти к описанию синтаксиса этого языка, посмотрим пример исходного кода.

Для этого создайте и запустите второй экземпляр Eclipse (Run -> Run Configurations…):



Во втором экземпляре Eclipse создайте новый myDSL-проект (File -> New -> Other… -> EMFText myDSL project):



На рисунке ниже вы видите пример кода, написанного на myDSL. Как видите, наш предметно-ориентированный язык действительно позволяет описывать сущности, свойства, типы данных.

Слева снизу синтаксическое дерево, которое соответствует метамодели языка. Снизу справа свойства одного из узлов дерева, которые также соответствуют метамодели. Если вы хотите получить какое-то другое синтаксическое дерево (добавить в него новые виды узлов, новые свойства узлов), то необходимо изменить метамодель языка.



Вы видите, что в редакторе есть подсветка синтаксиса. Позже мы её несколько усовершенствуем.

Также через Ctrl + Space вызывается автодополнение, которое по умолчанию работает не так как хотелось бы. Для атрибутов должны предлагаться только типы данных, а не сущности. Позже мы это исправим.



Описание конкретного синтаксиса


Теперь, когда вы увидели пример кода на тестовом DSL, вернёмся к описанию синтаксиса в файле myDSL.cs.



В строке 1 указано расширение файлов описываемого DSL.

В строке 2 указано пространство имен метамодели описываемого DSL.

В строке 3 указан начальный нетерминальный символ грамматики и по совместительству корневой метакласс синтаксического дерева.

В строке 6 указан один из параметров EMFText. Таких параметров порядка сотни, вы можете самостоятельно познакомиться с ними в руководстве.

Далее идут правила грамматики языка. Вы видите, что язык описания правил очень похож на EBNF. Однако, имена нетерминальных символов в левой части правила должны совпадать с именем некоторого метакласса из метамодели языка. А имена (не)терминальных символов в правой части правила должны совпадать с именами некоторых свойств этого метакласса.

Множественность символов в правой части правил должна соответствовать множественности соответствующих свойств в метамодели.

Разберём правила подробней.

В строке 10 мы утверждаем, что любой код на нашем DSL должен начинаться с ключевого слова «model», после которого может следовать описание нескольких типов. Причём, как вы должны помнить, в метамодели есть типы двух видов: сущности (Entity) и типы данных (DataType).

В строке 11 описан синтаксис для сущностей. Описание сущности может начинаться с ключевого слова «abstract», в этом случае одноименное свойство сущности в синтаксическом дереве будет установлено в истинное значение. Затем обязательно должно следовать ключевое слово «entity».

Затем следует имя сущности, которое будет сохранено в свойстве name. В квадратных скобках должен указываться вид токена для имен. В данном случае он не указан, поэтому парсер будет ожидать токен по умолчанию – TEXT. К токенам мы вернемся чуть позже.

Затем в фигурных скобках должны перечислять свойства (features) сущности. Это нетерминальный символ – в грамматике для свойств есть собственное правило (строка 13), а в метамодели – отдельный метакласс. Поэтому тут нет квадратных скобок, нет возможности указать вид токена.

В строке 12 описан синтаксис для типов данных. Описание типа данных должно начинаться с ключевого слова «datatype», после которого следует имя типа и точка с запятой.

В строке 13 описан синтаксис для свойств сущностей. Описание свойства может начинаться с одного из трёх ключевых свойств («att», «ref» или «cont»). В синтаксическом дереве в зависимости от указанного ключевого слова свойство kind узла примет одно из значений перечисления FeatureKind.

Далее должны следовать тип свойства, имя свойства и точка с запятой. Причём, тип свойства в коде указывается как строка символов, но в синтаксическом дереве ссылка по имени превращается в физическую ссылку на соответствующий тип. Таким образом, при парсинге мы получаем граф, а не дерево. К разрешению ссылок мы ещё вернёмся позже.

Вообще, что именно мы получаем при парсинге – не очень тривиальный вопрос. С одной стороны, полученная структура практически полностью дублирует грамматику языка и, вроде как, это конкретное синтаксическое дерево. С другой стороны, EMFText разрешает символьные ссылки, превращая конкретное синтаксическое дерево в абстрактный семантический граф. Также он позволяет прикручивать к парсеру постобработчики, с помощью которых можно упрощать модель.

Иными словами, парсер выдаёт на выходе какой-то гибрид конкретного синтаксического дерева и абстрактного семантического графа. Для такого простого языка это не очень принципиально. Но в следующей статье при разработке метамодели для SQL придётся снова вернуться к вопросу «какую метамодель мы делаем: конкретную или абстрактную?».

Добавление новых видов токенов


Теперь немного усовершенствуем DSL. В myDSL.cs после некоторых терминальных символов (name и type) стоят пустые квадратные скобки. Для таких символов используется токен по умолчанию TEXT с шаблоном

('A'..'Z'|'a'..'z'|'0'..'9'|'_'|'-')+

Это значит, что имена сущностей могут полностью состоять из десятичных цифр или начинаться с минуса, что, наверное, не очень правильно.

Примечание

Также имена сущностей не могут содержать не латинские буквы, наверное, нашему языку не помешала бы поддержка юникода.

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

Итак, пусть имена типов начинаются только с заглавной буквы латинского алфавита и не могут начинаться с других символов. А имена свойств – только со строчной буквы латинского алфавита.

Чтобы описать новые виды токенов, создаём раздел TOKENS (строки 9-16).



В строках 10-12 определены фрагменты токенов.

В строках 14 и 15 определены токены соответственно для имён типов и имён свойств.

В строках 20-22 в квадратных скобках указаны ожидаемые парсером токены.

Однако, есть проблема. Регулярные выражения для новых токенов пересекаются с токеном по умолчанию TEXT, о чём мы получаем предупреждение (см. рисунок выше). К чему это может привести?

Например, в исходном коде определена сущность «Car». Имя этой сущности соответствует обоим регулярным выражениям: TEXT и TYPE_NAME. Если лексер решит, что «Car» – это TYPE_NAME, тогда всё будет нормально. Но если он решит, что это TEXT, то на следующем этапе разбора исходного кода парсер выдаст ошибку типа такой: «После ключевого слова «entity» ожидается токен TYPE_NAME, а указан токен TEXT».

Примечание

Если вы не понимаете смысл предыдущего абзаца, то посмотрите рисунок в разделе «Немного теории» выше и прочитайте подразделы про лексический и синтаксический анализ.

Разрешить эту неопределенность можно несколькими способами:

  1. Положиться на то, что EMFText для более специфических токенов назначает по умолчанию больший приоритет. Т.е. сначала лексер будет искать TYPE_NAME и FEATURE_NAME, а потом TEXT.
  2. Задать приоритеты токенов вручную.
  3. Удалить лишние токены.
  4. Усложнить грамматику. Например, вместо «name[TYPE_NAME]» написать «name[TYPE_NAME] | name[TEXT]».

В данном случае, токен TEXT нам не нужен, поэтому мы его просто удалим. Для этого в строке 7 отключим, предопределенные токены: TEXT, LINEBREAK и WHITESPACE. Но два последних токена нам всё-таки нужны, поэтому определим их явно в строках 18 и 19.



Теперь кликните правой кнопкой мыши на проект в дереве слева и в появившемся контекстном меню выберите «Generate All (EMFText)». После перегенерации исходного кода запустите второй экземпляр Eclipse.

Теперь если вы напишите имя сущности со строчной буквы, то лексер интерпретирует его как имя свойства (FEATURE_NAME), а парсер выдаст ошибку, что ожидался токен TYPE_NAME.

Если вы начнёте имя атрибута с подчёрка «_», то лексер вообще не поймёт, что это за токен.



Подсветка синтаксиса


По умолчанию EMFText раскрашивает все ключевые слова фиолетовым цветом. Добавим немного больше цветов, для этого создайте секцию TOKENSTYLES (строки 22-27).



Перегенерируйте исходный код «Geneate All (EMFText)» и запустите второй экземпляр Eclipse.

Выглядит жутковато, но идею вы поняли :) Обратите внимание на то, что «car» раскрашивается синим, а не розовым цветом. Это связано с тем, что лексер выделяет токены с помощью контекстно-независимых регулярных выражений. Он не знает, что тут должно быть имя сущности, а не имя свойства.



Разрешение ссылок


Ранее я обращал ваше внимание на то, что автодополнение имён типов в определениях свойств сущностей работает не очень корректно. Для атрибутов (att) должны предлагаться только типы данных, а для ссылок (ref) и составных частей (cont) должны предлагаться только сущности.

Найдите в проекте org.emftext.language.myDSL.resource.myDSL класс FeatureTypeReferenceResolver, который отвечает за автодополнение и разрешение ссылок.

Метод resolve должен искать подходящие по имени типы. Если параметр resolveFuzzy имеет истинное значение, то метод должен искать типы, которые примерно подходят под заданную строку (это происходит при автодополнении имени типа). Иначе метод должен искать тип в точности с указанным именем.

Метод deResolve должен для ссылки в абстрактном семантическом графе возвращать её текстовое представление в исходном коде.

Вот, одна из реализаций разрешения ссылок на типы:

package org.emftext.language.myDSL.resource.myDSL.analysis;

import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;

import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.emftext.language.myDSL.DataType;
import org.emftext.language.myDSL.Entity;
import org.emftext.language.myDSL.EntityModel;
import org.emftext.language.myDSL.Feature;
import org.emftext.language.myDSL.FeatureKind;
import org.emftext.language.myDSL.Type;
import org.emftext.language.myDSL.resource.myDSL.IMyDSLReferenceResolveResult;
import org.emftext.language.myDSL.resource.myDSL.IMyDSLReferenceResolver;

public class FeatureTypeReferenceResolver implements IMyDSLReferenceResolver<Feature, Type> {

    // Ищем в модели типы с именем, указанным в параметре identifier
    public void resolve(String identifier, Feature container, EReference reference, int position, boolean resolveFuzzy,
            final IMyDSLReferenceResolveResult<Type> result) {

        // Не самый удачный способ искать корень синтаксического дерева.
        // Лучше у всех containment-ссылок в метамодели сделать обратную ссылку owner,
        // и переходить к корню через container.getOwner().getOwner()
        EntityModel model = (EntityModel) EcoreUtil.getRootContainer(container);

        // Если разрешаем ссылку на тип у атрибута, то ищем типы данных,
        // иначе ищем сущности
        Predicate<Type> isRelevant = container.getKind() == FeatureKind.ATTRIBUTE
                ? type -> type instanceof DataType
                : type -> type instanceof Entity;

        Stream<Type> types = model.getTypes().stream().filter(isRelevant);

        // С помощью этой функции будем добавлять подходящие типы в результаты поиска
        Consumer<Type> addMapping = type -> result.addMapping(type.getName().toString(), type);

        // Если поиск запущен из редактора при автодолнении имени, то ищем типы,
        // которые начинаются на искомую последовательность символов без учёта регистра
        if (resolveFuzzy) {
            types.filter(type -> type.getName().toUpperCase().startsWith(identifier.toUpperCase()))
                 .forEach(addMapping);
        }
        // Иначе (если это не автодополнение), то ищем тип в точности с указанным именем
        else {
            types.filter(type -> type.getName().equals(identifier))
                 .findFirst()
                 .ifPresent(addMapping);
        }
    }

    // Получаем текстовое представление ссылки на тип (его имя)
    public String deResolve(Type element, Feature container, EReference reference) {
        return element.getName();
    }

    public void setOptions(Map<?, ?> options) {
    }

}

Кодогенерация


С парсером и редактором в первом приближении разобрались. Осталась только кодогенерация.

Синтаксическое дерево в нижнем левом углу доступно только для просмотра. Чтобы получить возможность редактировать его, сохраните файл в формате xmi (File -> Save As…).

Если при этом произойдёт ошибка, что файл не может быть открыт с помощью MyDSLEditor, то проигнорируйте её и переоткройте xmi-файл. Вы увидите то же самое синтаксическое дерево, однако, теперь его можно редактировать.



Переименуйте сущность «Car» в «Vehicle» и установите истинное значение свойства «Abstract».

Сохраните xmi-файл с расширением myDSL. Закройте его и откройте снова:



Как видите, наши изменения синтаксического дерева учтены! Т.е. преобразование модели в текст (кодогенерация) работает.

Правда, при сохранении пропали переводы строк и некоторые пробелы.

Есть три способа добиться нормального форматирования генерируемого кода:

  1. Изменить метамодель, добавив каждому метаклассу ссылки на метакласс LayoutInformation из метамодели www.emftext.org/commons/layout. Я лично этого не делал и у меня ощущение, что при этом придётся считать количество требуемых пробелов, рассчитывать смещения в тексте и т.п. – выглядит очень сложно.
  2. Использовать отдельный форматировщик кода. Наверное, это оптимальный вариант при генерации Java-кода или чего-то, для чего уже есть готовый форматировщик.
  3. Добавить в грамматику языка несколько аннотаций, чтобы кодогенератор по умолчанию немного лучше форматировал код. Это самый простой вариант, так и сделаем.

В строки 30 и 31 добавлены аннотации «!0», «!1» и «#1». Эти аннотации игнорируются парсером, они предназначены для кодогенератора. Аннотация «#N» сообщает кодогенератору, что в данном месте необходимо вставить N пробелов. А аннотация «!N» обозначает перевод строки и N знаков табуляции.



Перегенерируйте исходный код «Geneate All (EMFText)» и перезапустите второй экземпляр Eclipse. Попробуйте снова сохранить модель в текстовом формате и убедитесь, что теперь код отформатирован лучше.

Заключение


После прочтения данной статьи вы должны по-новому взглянуть на разработку программного обеспечения – через призму моделей и преобразований моделей.

Модели можно представлять в разных нотациях (в виде диаграмм, таблиц, текста). С точки зрения разработки, управляемой моделями, предметно-ориентированный язык – это всего лишь одна из нотаций для некоторой метамодели.

С другой стороны, грамматика предметно-ориентированного языка – это метамодель в пространстве моделирования EBNF. А исходный код – модель в этом пространстве моделирования.

Парсер – это преобразование модели из пространства моделирования EBNF в модель в пространстве моделирования MOF или другом.

Кодогенератор – это обратное преобразование модели из семантико-ориентированного пространства моделирования (например, MOF) в пространство моделирования EBNF.

Также вы познакомились с одним из инструментов разработки языков программирования – EMFText.
+10
11.1k 66
Support the author
Comments 25
Top of the day