Pull to refresh

Создание языковых расширений в RASE. Часть 2. Создаем выражение

Reading time 9 min
Views 670
imageНашей исходной точкой будет проект, получившийся при написании прошлой статьи.
Итак, мы имеем в своем распоряжении маленький модуль, написанный на AS (две строки обрамленные разными кавычками), язык myLanguages.escapedStrings, в котором есть пока только два скрипта для автоматизации, относящиеся к аспекту Intentions: один обрабатывает строку в одинарных кавычках, а другой — строку в двойных кавычках.

Скриншот

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

С легкой печалью смотрим мы на нашу подопытную строку, сдобренную большим количеством слэшей. С одной стороны, задача решена, но с другой — как-то некрасиво, неправильно. Почему бы не предложить какой-нибудь несложный способ, при котором наша строка осталась бы прежней, но не вызывала бы ошибку «Incorrect string literal»? Нет ничего проще. Вспомним, что в некоторых других языках присутствует подобный функционал — например, в C# для таких имеется удобная конструкция @"… ", которая вполне подошла бы нам для портирования в качестве языкового расширения в ActionScript.
Не забываем, что в RASE исходный код приложения (с учетом всех используемых расширений) сперва обрабатывается генератором, который осуществляет создание исходного кода, понятного компилятору ActionScript. То есть мы сможем работать с нашей строкой в неизменном виде, а вот автоматическому экранированию она подвергнется только в момент генерации.


Шаг 1. За работу, товарищи!
Необходимо создать новое выражение. Для этого переходим в наше языковое расширение и в аспекте structure создаем новый концепт.

Скриншот

Вводим имя концепта — EscapedString.

Скриншот

Дальше встает вопрос, какую сущность он будет расширять? Чтобы ответить на него, можно попытаться провести небольшое исследование. Если наша строка заключена в двойные кавычки, то она относится к типу StringLiteral, а если в одинарные — то StringApostropheLiteral. Это предположение легко подтвердить, вызвав контекстное меню нашей строки и выбрав пункт Go to Concept Declaration (что равносильно сочетанию клавиш Ctrl-Shift-S или Cmd-Shift-S на Маке).

Скриншот

Скриншот

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

Скриншот

Скриншот

Мы видим, что StringLiteralBase, дочерними для которого будут StringLiteral и StringApostropheLiteral, сам наследуется от Expression, имплементируя интерфейс IStringLiteral.

Скриншот

Изучив структуру и методы концепта StringLiteralBase, принимаем решение наследоваться от Expression с интерфейсом IStringLiteral и встать на один уровень со StringLiteralBase. Возвращаемся к нашему недописанному руту и вводим (обратите внимание на написание, поддерживаемое «умным» автокомплитом по Ctrl-Space):

Скриншот

В окошке с иерархией, изменение сразу же отразится на отображаемом дереве:

Скриншот

Движемся далее. Следующим действием будет добавление алиаса в том же окне с нашим концептом. Он будет использоваться для автокомплита (чтобы добавить констукцию в код, пользователь должен будет ввести символ «@»). Для этой же цели добавляем shortDescription (это описание справа от алиаса в меню автокомлита).

Скриншот


Шаг 2. Создание редактора
Переходим к созданию редактора (в следующей вкладке).

Скриншот

Создаем в нем визуальное представление нашей будущей конструкции — заключенная в кавычки value, перед кавычками находится символ @

Скриншот

Сonstant в нашем скриншоте это специальная ячейка в которой хранится обычный текст. «Const» и «ctrl+space» скоро станет вашим частым действием при создании редактора.

Добавте еще ячейку (Enter) и выберите из списка автокомплита «{value}»

Скриншот

Добавляем еще одну кавычку-константу.

На этой стадии новый модуль имеет смысл первый раз откомпилировать. Нажимаем Ctrl-F9(Cmd+F9) или в главном меню выбираем Build > Make Module(s), после чего переходим в Main( ), чтобы создать в ней пример с новой строкой.

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

Скриншот

Скриншот


Шаг 3. Наводим порядок
Чтобы убрать лишние пробелы, возвращаемся к вкладке, в которой создавался редактор, и обращаем внимание на окно Inspector, в котором появляются стили каждого элемента. Можно заметить, что каждому элементу нашей конструкции соответствует свой набор свойств.

Скриншот

Уберем пробел между @ и открывающей кавычкой. Для этого выделим символ «собачки» и внесем в базовый стиль следующее свойство:

Скриншот

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

Скриншот

Скриншот

Там эти стили называются соответственно StringQuotationOpen и StringQuotationClose. Скопируем их в свой код. А за свойства поля value в нашем источике отвечает вот такая конструкция:

Скриншот

Снова компилируем модуль языкового расширения и заходим в класс Main (). Совсем другое дело!.

Скриншот


Шаг 4. Генерация кода
Теперь было бы любопытно посмотреть на AS-код, в который генерируется наше выражение. Из контекстного меню редактора выбираем Generate (obsolete) > Generate Text From Current Model (к слову, это же действие доступно внизу раздела Build главного меню).

Скриншот

Сгенерированный код можно посмотреть в левом нижнем углу экрана в окне Output. Что мы там видим?

Скриншот

Сообщение «TexGen not found» означает: редактор не может найти генератор, соответствующий нашему языковому расширению. Ах, да: мы же его еще не написали.

Скриншот

Добавляем новый генератор тем же путем, что мы создавали каждый новый аспект нашего расширения. Обычно название для генератора выбирается по имени целевого языка, в который он будет преобразовывать наш код. В нашем случае этим языком будет ActionScript.

Скриншот

Очевидно, что само по себе выбранное название еще не указывает ему цель, поэтому через контекстное меню переходим к свойствам этого аспекта и в нижнем окне вкладки Dependencies (полный путь Generator Properties > Dependencies > Depends on Generator) указываем ему зависимость от языка ActionScript.
Скриншот

В третьей вкладке свойств генератора указываем, на какой стадии в цепочке генерации кода всего проекта мы будем им пользоваться. В нашем случае — до ActionScript.

Скриншот

Настройки установлены, можно приступать к описанию логики генератора. Возвращаемся в рут EscapedString. Кликнув по пустому полю во вкладке Generator, вызываем Template Declaration.

Скриншот

Нашему взору открываются две закладки: первая называется main, а вторая — reduce_EscapedString. Как можно догадаться, первая содержит общие правила для нашей конструкции, а вторая — непосредственные операции над ней.

Скриншот

Скриншот

Переходим в закладку reduce_EscapedString и вставляем следующую конструкцию:

Скриншот
Получаем код обычной строки.

Template Fragment (доступен по комбинации клавиш Ctrl-Shift-F или через выпадающее меню Alt-Enter), это кусок кода или текста, который мы потом вставим в сгенерированный код.

Скриншот

Вокруг нашей строки появилась аннотация шаблона генератора.

Скриншот


Теперь нам нужно поместить значение в сгенеренную строку, для чего к нашей строчке добавляем property macro (выбираем готовый шаблон из списка intention для node.value):

Скриншот

Скриншот

И в результате, в случае компиляции модуля и генерации кода из Main () мы получили бы вот такой полуфабрикат:

Скриншот

Как мы видим, генератор пока что выгоняет обычный неэкранированный текст.

Для того чтобы добавить функционал экранирования, ставим курсор на символе «$» и открываем окно Inspector. Начинаем писать код, который изменит значение нашей строки при генерации:

Скриншот
Однако мы не будем описывать весь процесс экранирования в property macro, поскольку лучше создать метод у самой конструкции EscapedString и вызвать его из макро. Для создания методов у концептов специально предназначен аспект Behavior, в котором мы и создадим метод escape().

Шаг 5. Аспект Behavior
Теперь следует добавить метод escape() к нашему концепту.

Привычным жестом через контекстное меню нашего языка myLanguages.escapedStrings добавляем в проект аспект Behavior.

Скриншот

Наша задача — получить на выходе валидную строку, со всеми необходимыми слэшами.

Скриншот

Выделим this.value, с помощью соответствующего пункта из меню рефакторингов или клавиатурным сочетанием Сtrl-Alt-V (Cmd-Alt-V) обращаем его в локальную переменную result, которую будет выводить наш метод.

Скриншот

Скриншот

Скриншот

Чтобы не усложнять задачу, рабочий код позамиствуем из примера Intentions, приведенного в прошлой статье. В редакторе для этого существует клавиатурное сокращение Ctrl-E (Cmd-E), вызывающий окошко со списком недавно открытых файлов.

Скриншот

Не мудрствуя лукаво, копируем строчки кода из одного рута в другой (при копировании RASE спросит нас об импорте, но мы выберем Import None). Если вы создали проект с нуля, а не на основании учебного проекта, не забудьте импортировать язык Regexp с помощью шортката Ctrl-L (Cmd-L).

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

Скриншот

Чтобы получившийся код заработал, нам осталось только вернуться к аспекту Generator, где следует изменить в Инспекторе node.value на node.escape().

Скриншот

Скриншот

Компилируем модуль, генерируем ActionScript и смотрим на результат.

Скриншот


Шаг 6. Окно Type for Node
Мы добились определенных успехов в создании учебного языкового расширения, однако нельзя не заметить, что результат вышел несколько странным. Почему? Потому что от нашей строки пока немного практической пользы, да и если присмотреться повнимательнее, в текущем промежуточном итоге, у нас получилась не строка (тип String), а вообще непонятно что. Не верите — убедитесь сами, попробовав вызвать какое-нибудь действие сначала для соседних «обычных» строк…

Скриншот

… а затем для нашей:

Скриншот

Подтвердить неестественную сущность нашей строки можно, вызвав окно системы типов (Ctrl-Shift-T или Cmd-Shift-T), сообщающий, что у нашего выражения нет никакого типа:

Скриншот

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

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

Открыв закладку с кодом полюбившегося нам StringLiteralBase, можно обнаружить следующие конструкции:

Скриншот

Ага, перед нами Intention rule, как бы говорящий о том, что представляемая сущность является типом String. Здесь используется специальный язык Quotation («Цитирование»), позволяющие вставлять, например, код на ActionScript или MXML, тем самым добиваясь компактности и семантичности.
Создаем в нашем учебном языке аспект Typesystem.

Скриншот

Импортируем с помощью Ctrl-L (или Cmd-L) в наш проект язык Quotation и воспроизводим такую же точно логику в нашем языковом расширении.

Скриншот

В теле метода по аналогии со StringLiteralBase воспроизводим следующее:

Скриншот

Затем — это важно — вставляем ClassifierType...

Скриншот

По сочетанию клавиш Ctrl-R (Cmd-R) вызываем импорт модели.

Скриншот

Скриншот

И, наконец, можно добавлять String.

Скриншот

Для справки: примерно вот так выглядит аналогичный код без использования расширения Quotation.

Скриншот

Модуль можно компилировать — теперь наша сущность обрела свое место в системе типов ActionScript.

Скриншот


Шаг 7. Левые трансформации
Между тем, осталось еще несколько вещей, о которых следовало бы упомянуть в этой статье. В конце Шага 1 мы задавали алиас нашему выражению:

Скриншот

У редактора есть еще два важных поведения — это левые и правые трансформации (подробнее можно узнать о них в вводной статье про RASE). Говоря простыми словами, мы должны научить редактор превращать обычную строку в экранированную.

Добавляем в язык новый аспект Action командой transform menu actions.

Скриншот

Даем этой сущности имя makeStringEscaped и начинаем вносить правки. Нас интересует левая трансформация StringLiteralBase в EscapedString.

Скриншот

Скриншот

Добавляем simple item и начинаем заполнять следующий шаблон:

Скриншот

В итоге, у нас должен получиться примерно такой код:

Скриншот

Проект можно компилировать. Теперь при добавлении символа @ к любой строке, она моментально превратится в EscapedString.

Скриншот

Скриншот

Скриншот

Скриншот

Осталось добавить обратное действие — трансформацию сущности EscapedString в исходную строку при удалении @.


Шаг 8. Улучшаем поведение редактора
Заходим в Editor, где у нас уже имеется определенная конструкция:

Скриншот

Требуется добавить в нее некое ключевое слово, при удалении которого происходила бы обратная трансформация. Выбираем Create new > cell action map.

Скриншот

Теперь следует научиться обработать событие удаления символа @. Существует два способа — сложный (перехват ввода с клавиатуры) и простой (action map).

Скриншот

Выбор у нас невелик, выбираем DELETE и вводим код, который заменял бы EscapedString на StringLiteral.

Скриншот

В соседней вкладке выделяем поле, содержащее символ @, чтобы в окне Inspector назначить ему action map.

Скриншот

Чтобы наша трансформация заработала полноценно, добавим к свойствам соседнего поля (открывающая кавычка) параметр editable: false.

Скриншот

Учебное языковое расширение полностью готово.

Скриншот

Скриншот

Скриншот

Скриншот

9. Заключение
Мы надеемся, что после прочтении этой статьи создание языковых расширений перестало быть чем-то нереальным для обычного разработчика. Можно начать с малого и автоматизировать свою работу через intenions, не создавая языковые конструкции. А когда появится уверенность и реальная потребность, можно начать писать свои языки и DSL. Важно лишь научиться понимать простые принципы работы в MPS-платформе, ориентироваться в ней и находить готовые решения в других языках. В следующих статьях мы постараемся раскрыть эту тему еще глубже.
Tags:
Hubs:
+18
Comments 3
Comments Comments 3

Articles