Сообщений 0    Оценка 220        Оценить  
Система Orphus

Язык Nemerle

Часть 5

Автор: Чистяков Владислав Юрьевич
Источник: RSDN Magazine #2-2011
Опубликовано: 22.09.2011
Исправлено: 10.12.2016
Версия текста: 1.0
Макросы Nemerle
Что такое макрос?
Виды макросов
Макросы уровня выражения
Макросы верхнего уровня
Создание макросов
Macro Phase
Valid On
Параметры макроса
Простые примеры макросов
Получение информации об AST проекта
Получение информации о типах
Использование фазы компиляции WithTypedMembers
Макрос Disposable
Квази-цитирование
Цитаты
Сплайсы
Квази-цитаты с префиксом
Получение дополнительной информации о макросах
Отладка макросов
Заключение
Ссылки

Макросы Nemerle

ПРИМЕЧАНИЕ

Информация, описанная здесь, применима к Nemerle 1.1, так как использует некоторые новые имена типов и новые элементы пользовательского интерфейса, недоступные в Nemerle 1.0.

Что такое макрос?

ПРЕДУПРЕЖДЕНИЕ

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

Макрос можно (и нужно) рассматривать как модуль расширения компилятора.

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

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

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

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

ПРИМЕЧАНИЕ

Целевая сборка может компилироваться под платформу, отличную от платформы, на которой исполняется компилятор (например, целевая сборка предназначается для Silverlight, а компиляция производится под обычным .NET Framework). Макро-сборка же должна обязательно быть собрана для платформы, под которой запускается компилятор.

Сборка, содержащая хотя бы один макрос, называется макросборкой. Макросборки должны обязательно ссылаться на сборку Nemerle.Compiler.dll.

Если для работы с Nemerle вы используете VS (что является самым удобным способом разработки Nemerle-программ), то проще всего для создания макросборки использовать шаблон проекта «Macro Library»:


Рисунок 1. Шаблон проекта для макросборок.

Виды макросов

Макросы Nemerle можно разделить на макросы уровня выражения (expression level macro) и макросы верхнего уровня (top level macro, они же макроатрибуты).

Кроме того, макросы можно поделить на вводящие и не вводящие новый синтаксис. Причем синтаксис могут вводить как макросы уровня выражения, так и макросы верхнего уровня.

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

Макросы верхнего уровня могут применяться на уровне:

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

Макросы уровня выражения

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

СОВЕТ

Многие конструкции Nemerle являются макросами уровня выражения. Например, макросами уровня выражения являются все операторы циклов (foreach, for, while и т.п.), операторы &&, ||, if, return, continue, break, using, assert, late, lazy, ++, +=, => и т.п. Пример реализации макроса уровня выражения был приведен во второй части статьи.

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

ПРЕДУПРЕЖДЕНИЕ

Если вы не являетесь опытным писателем макросов Nemerle, то лучше взять за правило не делать внутри макроса уровня выражения никаких побочных эффектов.

Оптимально, когда макрос уровня выражения получает всю необходимую информацию из своих параметров или из поля UserData (хранящего словарь – System.Collections.IDictionary) типов ManagerClass (описывающего компилируемый проект) и TypeBuilder (описывающего тип, в контексте которого раскрывается макрос). Причем значения в эти словари можно помещать только из макроса верхнего уровня. Макросы уровня выражения должны использовать данные словари только на чтение.

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

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

Если вам нужно произвести некоторую настройку, проверку или создать некоторый тип, экземпляр которого будет хранить необходимую информацию во время исполнения целевой программы, то лучше разбить макрос на два. Первый оформить в виде макроса верхнего уровня. В этом макросе нужно произвести нужную инициализацию и создание типов. Второй оформить в виде макроса уровня выражения. В этом макросе нужно только генерировать код, который будет подставляться вместо макроса, а всю подготовительную деятельность, вызывающую побочные эффекты, перенести в первый макрос. Данные между этими макросами можно передавать через свойство TypeBuilder.UserData[...] или ManagerClass.UserData[...].

Макросы верхнего уровня

Макросы верхнего уровня выполняются в момент, когда компилятор обрабатывает конструкции верхнего уровня целевой программы, такие как:

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

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

Безопасность макросов верхнего уровня обусловлена тем, что перед их запуском компилятор производит парсинг всех файлов проекта и строит новую копию AST верхнего уровня (структуры данных, описывающей проект).

Изменение AST верхнего уровня влечет за собой его полное перестроение, и, таким образом, макросы верхнего уровня запускаются на «свежем» AST.

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

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

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

  1. Кэшировать результаты вычислений (например, во внешних файлах) и производить их только тогда, когда кэш устарел.
  2. Проверять, что работа макроса осуществляется под управлением IDE (с помощью свойства ManagerClass.IsIntelliSenseMode) и не производить сложных вычислений. Например, если вам нужно сгенерировать класс, реализующий парсер на основании формально заданной грамматики, то в режиме IDE можно сгенерировать только заглушки для методов этого класса (оставив их тела пустыми), а генерацию тел методов (и сопутствующие этому сложные расчеты) производить только когда макрос работает под управлением компилятора. Это существенно повысит производительность макроса. При этом, однако, можно, например, произвести проверку грамматики и выдать пользователям макроса сообщения об ошибках, если таковые имеются.

Макросы уровня выражения удобны для описания DSL, генерирующих/изменяющие типы или их члены.

Создание макросов

Для создания макросов лучше всего использовать wizard создания макроса, входящий в состав интеграции Nemerle для VS 2010.

Чтобы запустить этот wizard, нужно в VS 2010 выбрать подходящую ветку в проекте макро-библиотеки и выбрать пункт меню «Project\Add New Item...» (или пункт «Add\New Item...» из контекстного меню в Solution Explorer). В появившемся диалоге (см. рисунок 2) нужно ввести имя для нового макроса (по совместительству, имя файла, в котором он будет расположен), выбрать шаблон «Macro wizard» и нажать кнопку «Add».


Рисунок 2. Окно «Add New Item».

После нажатия кнопки «Add» на экране появится диалоговое окно «Macro Wizard», приведенное на рисунке 3.


Рисунок 3. Оно «Macro Wizard».

С помощью выпадающего списка «Macro Type» можно выбрать, какой тип макроса необходимо создать:

Macro Attribute – создать макрос верхнего уровня.

Expression level – создать макрос уровня выражения.

Если выбран тип «Expression level», то все элементы управления, кроме переключателя Define Syntax Extension делаются недоступными, так как к макросам уровня выражения неприменимы понятие фаза компиляции (Macro Phase) и область применения (Valid On). Если же выбрать тип «Macro Attribute», то эти элементы управления, напротив, становятся доступными.

«Macro Phase» и «Valid On» задают значения соответствующих параметров атрибута Nemerle.MacroUsageAttribute.

Разберем их значения подробнее.

Macro Phase

Выпадающий список «Macro Phase» позволяет задать стадию компиляции, на которой будет вызываться макро-атрибут. Всего есть три фазы:

Выбор фазы

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

Один макрос, работающий на нескольких фазах

Несколько макросов могут иметь одно имя, если они имеют разные фазы запуска или разные области применения. Зачастую бывает выгодно создать не один большой макрос, делающий всю работу, а несколько макросов поменьше и разбить тем самым задачу на подзадачи. Так, полезным приемом является разбиение макроса на два (с одним именем), выполняющиеся на разных стадиях. Макрос, работающий на стадии BeforeInheritance, может добавлять реализацию интерфейсов, новые члены (например, для добавленных интерфейсов), а макрос, работающий на стадии WithTypedMembers, может генерировать реализацию для методов, добавленных на стадии BeforeInheritance.

Предположим, что мы хотим создать макро-атрибут, применимый к классу, который добавлял бы к классу реализацию интерфейса IDisposable, автоматически вызывающую метод Dispose у всех полей и свойств, объявленных в классе, а также вызывающую базовую реализацию метода Dispose, если класс, от которого унаследован текущий класс, также реализовывал интерфейс IDisposable. В этом случае удобно создать два макроса. Один – макрос, применимый к классу, и работающий на стадии BeforeInheritance, будет добавлять реализацию интерфейса IDisposable и метод Dispose (оставляя при этом его тело пустым). Второй – применимый к методу и работающий на стадии WithTypedMembers, может реализовывать тело метода Dispose, добавленного предыдущим макросом. При этом первый макрос, создавая метод Dispose, может просто добавить к нему второй мета-атрибут, что приведет к его раскрытию в будущем. Мы разберем этот пример подобнее в разделе «Пример реализации интерфейса IDisposable».

Valid On

Выпадающий список «Valid On» позволяет задать тип элемента AST, к которому может быть применим макро-атрибут. Возможные значения:

Все значения, кроме значения «Type», соответствуют значениям перечисления MacroTargets. Значение «Type» соответствует значению MacroTargets.Class. В wizard-е выбрано название «Type», так как оно лучше отражает действительность, поскольку определяет применимость к любому типу, а не только к классу.

Параметры макроса

Макрос может, а порой просто обязан, иметь параметры. Параметры макросов можно разделить на обязательные и не обязательные (дополнительные, пользовательские).

Обязательные параметры макроса

Макросы верхнего уровня типа: Type, Method, Property, Field и Event должны иметь параметры через которые макросу передаются ссылки на Builder-ы (т.п. типы формирующие некоторые сущности) – сущностей, к котором применяются макросы. Через них макрос может получить информацию о сущностях, к которым он применен, или изменять эти сущности. В таблице 1 приводится список типов обязательных параметров для макросов верхнего уровня (макроатрибутов). Типы идут в той последовательности, в которой должны идти параметры.

Макросы уровня выражения и типа Assembly не дополнительных параметров, так как не применяются непосредственно к каким либо сущностям.

Таблица 1. Обязательные параметры для макросов верхнего уровня.

Применимо к / Фаза

BeforeInheritance

BeforeTypedMembers

WithTypedMembers

Assembly

-

-

-

Type

TypeBuilder

TypeBuilder

TypeBuilder

Method

TypeBuilder, ClassMember.Function

TypeBuilder, ClassMember.Function

TypeBuilder, MethodBuilder

Field

TypeBuilder, ClassMembe.Field

TypeBuilder, ClassMembe.Field

TypeBuilder, FieldBuilder

Property

TypeBuilder, ClassMember.Property

TypeBuilder, ClassMember.Property

TypeBuilder, PropertyBuilder

Event

TypeBuilder, ClassMember.Event

TypeBuilder, ClassMember.Event

TypeBuilder, EventBuilder

Parameter

TypeBuilder, ClassMember.Function, PParameter

TypeBuilder, ClassMember.Function, PParameter

TypeBuilder, MethodBuilder, TParameter

Где:

Дополнительные параметры макроса

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

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

ПРЕДУПРЕЖДЕНИЕ

Обратите внимание на этот факт! Это очень важно для понимания сути макросов.

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

Надо четко понимать, что макрос – это сущность времени компиляции и ему можно передать только то, что существует во время компиляции. А во время компиляции существуют только AST и информация о типах, используемых в проекте. Причем передать макросу в качестве параметров можно только AST.

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

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

Ниже приведен список типов, допустимых для дополнительных параметров макросов:

В составе AST выражений Nemerle (как и в других языках) есть литеральные значения (литеральные константы вроде «"строка"» или «42»). Их можно использовать для передачи макросам некоторой информации. Например, если макросу требуется путь к некоторому файлу, то его можно оформить в виде строкового литерала. Макрос может производить анализ своих параметров и, если в параметре оказался не строковый литерал, а другое выражение, выдавать сообщение об ошибке. Однако такой анализ требует написания некоторого малополезного кода. Чтобы избавить писателя макроса от этой бесполезной работы, в состав поддерживаемых типов параметров макросов были введены следующие типы: bool, byte, decimal, double, float, int, long, sbyte, short, string, uint, ulong и ushort.

ПРЕДУПРЕЖДЕНИЕ

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

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

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

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

Простые примеры макросов

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

Создайте новое решение с именем «MacroIntro» и новым проектом на основе шаблона «Macro Library» с именем «MacroIntroLibrary» (см. рисунок 4). Для этого выберите в меню «File» пункт «New\Project...» или нажмите Ctrl+Shift+N.


Рисунок 4. Создание решения «MacroIntro».

В это решение добавьте еще один проект (меню «File\Add\New Project...» или «Add\New Project...» из контекстного меню решения) на основе шаблона «Console Application». Этому проекту дайте имя «Test».

В проект Test нужно добавить ссылку на проект «MacroIntroLibrary» (см. рисунки 5 и 6).


Рисунок 5. Добавление ссылки на MacroIntroLibrary.

ПРИМЕЧАНИЕ

Вообще-то, по уму, добавлять макросборки лучше было бы в ветку «Macro Reference». Сборки, добавленные в «Macro Reference», передаются компилятору именно как макросборки. Это предотвращает от использования типов, находящихся в них в рамках целевых проектов. Но в текущей реализации интеграции для VS 2010 и VS 2008 имеется неприятная ошибка. Ошибка заключается в том, что сборки, добавленные в «Macro Reference», не учитываются в списке зависимостей проектов. Это приводит к тому, что после изменения макропроектов проекты, где имеются на них ссылки в разделе «Macro Reference», не обновляются автоматически и не перекомпилируются. Это некритично при использовании «чужих» (внешних) макросборок, но очень неудобно при разработке собственных макросборок. Поэтому на время отладки лучше использовать ссылку в разделе «Reference» (по крайней мере, до исправления указанного недостатка).


Рисунок 6. Добавление ссылки на MacroIntroLibrary – диалог «Add Reference».

После окончания данной операции список ссылок проекта Test должен выглядеть так, как показано на рисунке 7.


Рисунок 7. Список ссылок проекта Test после добавления ссылки на проект MacroIntroLibrary.

Чтобы макросы, описанные в проекте MacroIntroLibrary, стали доступны в проекте Test, нужно пересобрать решение.

ПРЕДУПРЕЖДЕНИЕ

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

Итак, мы закончили подготовительный этап. Теперь можно приступать к экспериментам с макросами.

Макрос не обязан что-то менять или генерировать. Он может использоваться как средство анализа имеющегося кода или даже наблюдения за проектом. Давайте сделаем первый макрос который выведет информацию о имеющихся в проекте типах.

Для этого воспользуйтесь макро- wizard-ом (как описано выше) и создайте в проекте MacroIntroLibrary макроатрибут с именем ProjectInfo. Он должен применяться к сборке («Macro Type» = «Macro Attribute», «Valid On» = «Assembly») и работать на фазе BeforeInheritance. Дополнительных параметров описывать не надо.

По окончании работы wizard-а должен создаться файл ProjectInfo.n со следующим содержанием:

      using Nemerle;
using Nemerle.Collections;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Compiler.Typedtree;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Linq;


namespace MacroIntroLibrary
{
  [MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Assembly)]
  macro ProjectInfo()
  {
    ProjectInfoImpl.DoTransform(Macros.ImplicitCTX(), )
  }
  
  module ProjectInfoImpl
  {
    public DoTransform(typer : Typer, ) : void
    {
      Macros.DefineCTX(typer);
      // TODO: Add implementation here.
      ;
    }
  }
}
ПРИМЕЧАНИЕ

Современная версия интеграции с VS не поддерживает IntelliSence внутри макросов. По этому макро-wizard формирует модуль с одним единственным методом, который и вызывается из макроса. Внутри этого метода доступен IntelliSence.

СОВЕТ

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

Описание макроса начинается с ключевого слова «macro» и состоит из заголовка и тела. Кроме того, к макросу применимы пользовательские атрибуты (но реально потребуется использовать только атрибут MacroUsage).

Формальный синтаксис описания макросов следующий:

Macro      = Attributs? "macro" Name "(" Parameters? ")""{" Expression "}";
Name       = Identifier;
Parameters = Parameter ("," Parameter) ","?;
Parameter  = Identifier (":" Type)? ("=" Default);
Type       = "PExpr" | "Token" | "params array[PExpr]" | "params list[PExpr]" 
           | "bool" | "byte" | "decimal" | "double" | "float" | "int" | "long" 
           | "sbyte" | "short" | "string" | "uint" | "ulong" | "ushort";

Default    = "<[" PExpr "]>"// цитата кода
           |  Literal;       // число или строка
Expression = PExpr;

Код, сгенерированный wizard-ом, должен быть вам понятен. Пояснения требуют только две его строки.

ImplicitCTX – это макрос из стандартной библиотеки, позволяющий получить текущий типизатор (объект типа Nemerle.Compiler.Typer). Типизатор – это объект компилятора, вызывающий (раскрывающий) макрос. Через этот объект макрос может получать дополнительную информацию о проекте и пользоваться сервисами компилятора.

Строка:

      Macros.DefineCTX(typer);

задает так называемый неявный контекст компилятора. Реально она просто определяет локальную неизменяемую переменную типа Typer. Эта переменная используется некоторыми сервисами, предоставляемыми компилятором. Позже будет показано, в каких случаях данная строка необходима.

Получение информации об AST проекта

Измените тело метода ProjectInfoImpl.DoTransform, как показано ниже:

Macros.DefineCTX(typer);

def types = typer.Manager.NameTree.NamespaceTree.GetTypeBuilders( // 1
              onlyTopDeclarations=true);

foreach (type in types)
{
  def partsCount = type.AstParts.Length;

  Message.Hint(type.AstParts.Head.Location, // 2
    $"Type: '$(type.FullName)'  Parts count: $partsCount");

  foreach (ast in type.AstParts with i) // 3
  {
    Message.Hint(ast.Location, // 4
      $"  Part $(i + 1) of type '$type'  $(ast.GetMembers().Length)");

    foreach (memberAst in ast.GetMembers()) // 5
    {
      Message.Hint(memberAst.NameLocation, $"    $memberAst"); // 6match (memberAst) // 7
      {
        | <[ decl: ..$_attrs $_name(..$parameters) : $retType $_body ]> => // 8foreach (param in parameters with i)
            Message.Hint(param.Location, $"      Paramert $(i + 1): $param"); // 9

          Message.Hint(retType.Location, $"      Return type $retType"); // 10
        
        | _ => ()
      }
    }
  }
}

Этот код выводит информацию о типах, объявленных в проекте, и их членах. Информация выводится в окно «Output» VS и в «Error list». Это позволяет двойным щелчком по строке с информацией перемещаться к соответствующей декларации в коде проекта.

Давайте разберем подробнее, что же делает данный макрос.

Typer – это, пожалуй, главный объект, передаваемый (неявно) макросу. Typer создается для типизации методов, функций или отдельных выражений. У Typer есть свойство Manager. Это свойство хранит ссылку на объект (ManagerClass), содержащий общую информацию обо всем компилируемом проекте. В числе прочего этот объект хранит дерево типов. У дерева типов можно запросить список TypeBuilder-ов. Это делается путем вызова метода GetTypeBuilders. Его параметр onlyTopDeclarations позволяет указать, нужно ли получать только типы верхнего уровня, или все типы, включая вложенные.

ПРИМЕЧАНИЕ

Типизация – это процесс вычисления дополнительных атрибутов – типов. В процессе типизации компилятор вычисляет истинное значение конструкций программы. Например, он определяет, что одно выражение является вызовом функции, а другое – вызовом конструктора. В процессе типизации производится разрешение перегрузок и других неоднозначностей. В конце процесса типизации компилятор генерирует так называемое типизированное AST. Для выражений типизированное AST описывается типом TExpr.

Более подробную информацию о типизации можно почерпнуть из четвертой части статьи «Макросы Nemerle – расширенный курс».

TypeBuilder (из пространства имен Nemerle.Compiler) – это объект, описывающий типы, определенные в проекте (разобранные из кода, входящего в проект, или созданные макросами). Для каждого типа, определенного в проекте, создается один TypeBuilder. Если тип является partial-типом (т.е. состоит из нескольких частей), то для него все равно создается один объект TypeBuilder, в который помещается информация обо всех частях.

Таким образом, строка 1 получает все типы проекта за исключением вложенных.

На фазе BeforeInheritance доступно только AST. AST типов доступно через свойство AstParts объекта TypeBuilder. AstParts хранит список TopDeclaration из пространства имен Nemerle.Compiler.Parsetree. TopDeclaration – это вариантный тип, описывающий AST деклараций верхнего уровня (типов).

ПРИМЕЧАНИЕ

Свойство AstParts имеет такое название и хранит список потому, что типы в Nemerle могут состоять более чем из одного описания. Такие типы называются partial-типами (частичными типами). Каждая часть частичного типа должна содержать в своем описании модификатор «partial». AstParts хранит список таких частей. Если тип не является частичным, то это свойство содержит один элемент. Кроме свойства AstParts в TypeBuilder есть так же свойство Ast. Оно хранит AST первой встретившейся компилятору части. Однако в макросах лучше всегда использовать свойство AstParts, чтобы не породить (по случайности) код, который не может работать с частичными типами.

Строка 2 выводит информационное сообщение компилятора (Hint) в котором выводится имя типа и количество его частей.

ПРИМЕЧАНИЕ

Макрос Nemerle может взаимодействовать с компилятором. В частности, он может выводить сообщения: сообщения об ошибках (errors), предупреждения (warnings) и информационные сообщения (hints/messages/подсказки). Сообщения появляются в логе компилятора и в IDE. В IDE сообщения отображаются в окне «Error list» и в виде ребристых разноцветных подчеркиваний, при наведении на которые мыши появляются всплывающие подсказки с описаниями.

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

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

Для вывода сообщения используется объект Message (из пространства имен Nemerle.Compiler).

Для вывода сообщений об ошибках можно использовать методы Error, FatalError и FatalError2 (на самом деле FatalError – это макрос, но это не так важно).

Для вывода предупреждений используется метод Warning. Для вывода информационных сообщений используются методы Hint и HintOnce. Последний выводит сообщение с заданным текстом или номером один раз.

В качестве параметров всем методам можно передать строку с сообщением и местоположение (структуру Location из пространства имен Nemerle.Compiler). Для предупреждений и сообщений также можно задать номер. Номер сообщения – это положительное целое число, которое выводится в сообщении, и которое может быть использовано пользователем для блокирования вывода этого сообщения.

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

Выражение «type.AstParts.Head» возвращает первый элемент списка частей AST типа. А свойство Location возвращает одноименную структуру, описывающую местоположение участка кода.

ПРИМЕЧАНИЕ

Структура Location (из пространства имен Nemerle.Compiler) определяет файл и местоположение в нем. Данная структура хранит имя (File) файла и индекс (FileIndex) файла (в Nemerle каждому файлу, открытому в процессе компиляции или в IDE, сопоставляется определенный индекс), начальная строка (Line), начальная колонка (Column), конечная строка EndLine, конечная колонка EndColumn. Также можно получить начальную (Begin) и конечную (End) текстовую точку (структуру TextPoint из пространства имен Nemerle.Compiler).

Данная структура используется для запоминания местоположения кода. Почти любой элемент, описывающий код в Nemerle, снабжен свойством Location и другими свойствами типа Location. Это позволяет указывать в сообщениях точные координаты проблемного места. А это, в свою очередь, упрощает жизнь конечному пользователю. Старайтесь всегда указывать местоположение в сообщениях. Если не указать местоположение в сообщении, компилятор будет использовать так называемый стек местоположений (Location-стек), который заполняет компилятор. Но компилятор не знаком с деталями проблемы, выявленной макросом, и помещает в стек только местоположение всего раскрываемого макроса.

Вы тоже можете изменять стек местоположений. Для этого существует макрос locate из пространства имен Nemerle.Compiler.Util.

Таким образом, сообщение, выводимое в строке 2, будет указывать на первую часть объявления типа. Само сообщение будет содержать имя типа и количество частей, из которых он состоит.

В строке 3 происходит перебор всех частей, из которых состоит тип.

СОВЕТ

Конструкция with «имя индекса» в цикле foreach позволяет получить не только элементы перебираемого списка, но и индексы этих элементов. Это удобно, если нужно вывести порядковые имена, или при работе с массивами. Подобные вещи возможны благодаря тому, что foreach также является макросом, и расширение его функциональности является относительно несложной задачей, которую может выполнить любой пользователь. Удачные расширения после обсуждения сообществом добавляются в стандартную библиотеку макросов. Сделать свое предложение можно, создав pull request на https://github.com/rsdn/nemerle/ и создав соответствующее сообщение на англоязычном форуме или на RSDN.

Строка 4 выводит информацию об отдельных частях, из которых состоит тип. Это позволит перейти к их описанию. Из сообщения можно будет также узнать, сколько членов определено в каждой из частей. Для этого используется метод GetMembers. Он возвращает список ClassMember. ClassMember – это вариантный тип данных, описывающий AST членов типов (методов, свойств, полей, событий, вложенных типов).

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

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

Строки 7 и 8, пожалуй, самые интересные. В них производится разбор AST членов типа с помощью сопоставления с образцом. При этом используется образец в виде квази-цитаты (строка 8).

СОВЕТ

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

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

Квази-цитата из строки 8 описывает образец для декларации метода. С этим образцом сопоставится AST любого метода. Информация из сплайсов (конструкций вида $x и ..$x) используется для получения информации о частях AST, которая затем выводится в сообщениях. Обратите внимание, что эта информация также является ветвями AST. $x и ..$x отличаются тем, что $x используется для подстановки или распознавания единичного выражения, а ..$x – для списка выражений. Более подробно о сплайсах говорится в разделе «Сплайсы».

СОВЕТ

В приведенной цитате имеются сплайсы, содержащие имена, начинающиеся со знака подчеркивания (_attrs, _name и _body). Имена в сплайсах образцов оператора match вводят локальные переменные. Если локальная переменная объявляется, но не используется, компилятор Nemerle выдает об этом предупреждение. В Nemerle принято соглашение, по которому имена, имеющие вначале символ подчеркивания, игнорируются компилятором (он не выводит предупреждение об их неиспользовании).

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

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

ПРЕДУПРЕЖДЕНИЕ

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

Строка 9 выводит информацию о параметрах. Строка 10 – о возвращаемом значении.

Чтобы применить этот макрос, измените файл Main.n из проекта Test.nproj следующим образом:

        using Nemerle.Collections;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Console;
using System.Linq;

using MacroIntroLibrary; // 1

[assembly: ProjectInfo] // 2publicpartialclass TestClass { }

module Program
{
  Main() : void
  {
    WriteLine("Hi!");
    _ = ReadLine();
  }
}

publicpartialclass TestClass
{
  public Method(param1 : int, param2 : System.Int32, param3 : Int32) : int
  {
    param1 + param2 + param3
  }
}

В строке 1 открывается пространство имен, в котором объявлен макрос.

В строке 2 производится обращение к макросу. Нетрудно заметить, что оно полностью аналогично пользовательским атрибутам (далее просто «атрибутам») C# и Nemerle. Но, в отличие от простых атрибутов, данный не приводит к генерации атрибутов в метаинформации сборки, а приводит к запуску макроса.

Откомпилируйте решение. При этом в окнах «Error List» и «Output» будут выведены информационные сообщения. Ниже приведено содержимое из окна «Output».

C:\...\Main.n(14,1): warning : hint: Type: 'TestClass' Parts count: 2
C:\...\Main.n(14,1): warning : hint:   Part 1 of type 'TestClass'  0
C:\...\Main.n(25,1): warning : hint:   Part 2 of type 'TestClass'  1
C:\...\Main.n(27,10): warning : hint:     Function: public Method(param1 : int, param2 : System.Int32, param3 : Int32) : int;
C:\...\Main.n(27,17): warning : hint:       Paramert 1: param1 : int
C:\...\Main.n(27,31): warning : hint:       Paramert 2: param2 : System.Int32
C:\...\Main.n(27,54): warning : hint:       Paramert 3: param3 : Int32
C:\...\Main.n(27,72): warning : hint:       Return type int
C:\...\Main.n(16,1): warning : hint: Type: 'Program' Parts count: 1
C:\...\Main.n(16,1): warning : hint:   Part 1 of type 'Program'  1
C:\...\Main.n(18,3): warning : hint:     Function: static Main() : void ;
C:\...\Main.n(18,12): warning : hint:       Return type void

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

Получение информации о типах

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

Это можно сделать двумя способами:

Ручное связывание

Для начала попробуем первый способ. Для этого немного изменим макрос после строки 8:

| <[ decl: ..$_attrs $_name(..$parameters) : $retType $_body ]> => // 8foreach (paramin parameters with i)
  {
    def paramType = typer.BindType(param.Type);
    Message.Hint(param.Location, $"      Paramert $(i + 1): $(param.Name) : $paramType"); // 9
  }

Если теперь перекомпилировать проект, то информация о параметрах метода изменится на:

C:\...\Main.n(27,10): warning : hint:     Function: public Method(param1 : int, param2 : System.Int32, param3 : Int32) : int;
C:\...\Main.n(27,17): warning : hint:       Paramert 1: param1 : int
C:\...\Main.n(27,31): warning : hint:       Paramert 2: param2 : int
C:\...\Main.n(27,54): warning : hint:       Paramert 3: param3 : int

Как видите, типы параметров стали выглядеть одинаково (int, а не int, System.Int32, Int32). Но главное не то, что мы получили красивое представление, а то, что вместо AST мы получили ссылку на тип. Как и типы .Net, типы Nemerle представляются объектами. С этими объектами можно выполнять разные операции. Можно точно определить, что тип – это ожидаемый вами тип, или проверить может ли тип быть подтипом (или супертипом) другого типа.

ПРЕДУПРЕЖДЕНИЕ

В макросах Nemerle нельзя манипулировать типами .Net! На то есть сразу несколько причин:

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

2. Система типов Nemerle богаче, чем система типов .Net. Nemerle поддерживает такие типы, как: вариантные типы, кортежи, функциональные типы, псевдонимы типов.

3. Макросы раскрываются в процессе так называемого процесса типизации. При этом типы еще могут быть не выведены или выведены только частично. Попытка запросить «системный» тип может привести к проблемам в работе алгоритма вывода типов.

Типы в Nemerle выражаются двумя типами (простите за тавтологию), описанными в пространстве имен Nemerle.Compiler:

Возвращаемым значением метода BindType является TypeVar.

СОВЕТ

Воспользуйтесь интеграцией с VS, чтобы подсматривать типы. Если навести курсор мыши на некоторый идентификатор в коде, то появится всплывающая подсказка, в которой будет дано подробное описание типа, соответствующего выбранному идентификатору. Так, если навести курсор мыши на метод BindType, то вы увидите всплывающую подсказку, показанную на рисунке 8. Если подвести курсор мыши к именам типов (подсвеченным цветом морской волны), то появятся дополнительные подсказки, предоставляющие информацию об этих типах.

«Погуляйте» по коду макроса, чтобы выяснить какие типы имеют те или иные элементы кода.


Рисунок 8. Всплывающая подсказка с описанием типов.

Это позволяет связывать частично заданные типы или даже вообще не заданный тип (да, да, в Nemerle и так может быть). Частично заданный тип – это тип, в котором имя типа и/или некоторое параметры обозначены подстановочным символом «_». Все выражение, описывающее тип, задано одним подстановочным символом, BindType возвратит так называемую свежую переменную типа (новый объект TypeVar, у которого свойство IsFresh равно true).

Ниже приведены примеры частично заданных типов:

Dictionary[_, int] // не задан первый параметр типов (TKey)
Dictionary[_, _] // не заданы оба параметра типов.// не задан параметр типов типа вложенного во второй тип
Dictionary[string, System.Collections.Generic.List[_]]
_? // не задан тип, но наложено ограничение что это должен быть Nullable-тип

Основной особенностью TypeVar является то, что он поддерживает так называемую унификацию (как в языке Prolog). Унификация позволяет, с одной стороны, проверить совместимость типов (это можно сделать с помощью методов TryUnify, TryRequire и TryProvide), а с другой – наложить на типы ограничения. Об этом будет рассказано позже.

Вместо метода BindType можно воспользоваться методом BindFixedType. Он возвращает FixedType и не допускает использование «_».

В примере выше можно заменить BindType на BindFixedType без каких бы то ни было видимых последствий. Но если впоследствии изменить описание параметра так, чтобы его тип не был задан явно:

          public Method(param1 : int, param2 : System.Int32, param3 = 42) : int
  {
    param1 + param2 + param3
  }

то вместо связывания будет выдано сообщение об ошибке:

C:\...\Main.n(27,54): error : type inference not allowed here

Использование фазы компиляции WithTypedMembers

Чтобы продемонстрировать использование фазы компиляции WithTypedMembers, создадим еще один макрос с именем ProjectTypeInfo. Снова воспользуйтесь Macro wizard, чтобы создать новый макрос. На этот раз в выпадающем списке «Macro Phase» вместо BeforeInheritance выберите «WithTypedMembers». Все остальные настройки оставьте как в предыдущем случае. В результате у вас должна появиться следующая заготовка для макроса:

        using Nemerle;
using Nemerle.Collections;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Compiler.Typedtree;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Linq;

namespace MacroIntroLibrary
{
  [MacroUsage(MacroPhase.WithTypedMembers, MacroTargets.Assembly)]
  macro ProjectTypeInfo()
  {
    ProjectTypeInfoImpl.DoTransform(Macros.ImplicitCTX(), )
  }
  
  module ProjectTypeInfoImpl
  {
    public DoTransform(typer : Typer, ) : void
    {
      Macros.DefineCTX(typer);
      // TODO: Add implementation here.
      ;
    }
  }
}

Поместите в метод DoTransform следующий код:

Macros.DefineCTX(typer);
      
def types = typer.Manager.NameTree.NamespaceTree.GetTypeBuilders( // 1
        onlyTopDeclarations=true);

foreach (typein types)
{
  Message.Hint(type.Location, // 1
    $"Type: '$(type.FullName)' : $(type.BaseClass)");

  def bindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance
    | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
        
  def members = type.GetMembers(bindingFlags); // 2foreach (member in members)
  {
    Message.Hint(member.NameLocation, $"    $member"); // 3match (member)
    {
      | methodis MethodBuilder =>
        def header = method.Header;
        foreach (param in header.Parameters with i) // 4
          Message.Hint(param.Location,
            $"      Paramert $(i + 1): $(param.Name) : $(param.Type)"); // 5
              
        Message.Hint(header.ReturnTypeLocation,
          $"      Return type $(header.ReturnType)"); // 6

      | _                       => ()
    }
  }
}

Теперь замените в файле Main.n из проекта Test.nproj строку 2 на:

[assembly: ProjectTypeInfo] // 2

и перекомпилируйте проект.

В окне «Output» вы увидите следующее:

C:\...\Main.n(14,1): warning : hint: Type: 'TestClass' : object
C:\...\Main.n(14,1): warning : hint:     constructor TestClass..ctor() : TestClass
C:\...\Main.n(14,1): warning : hint:       Return type void
C:\...\Main.n(27,10): warning : hint:     method TestClass.Method(param1 : int, param2 : int, param3 : int) : int
C:\...\Main.n(27,17): warning : hint:       Paramert 1: param1 : int
C:\...\Main.n(27,31): warning : hint:       Paramert 2: param2 : int
C:\...\Main.n(27,54): warning : hint:       Paramert 3: param3 : int
C:\...\Main.n(27,69): warning : hint:       Return type int
C:\...\Main.n(16,1): warning : hint: Type: 'Program' : object
C:\...\Main.n(18,3): warning : hint:     method Program.Main() : void
C:\...\Main.n(18,12): warning : hint:       Return type void

Как видите, результат очень похож на то, что порождал макрос ProjectInfo, но код макроса ProjectTypeInfo больше похож на использование рефлексии (System.Reflection).

Если приглядеться внимательно, то можно найти отличия и в выводимой информации. Так, строка, описывающая метод «Method», раньше выглядела так:

Function: public Method(param1 : int, param2 : System.Int32, param3 : _ ) : int;

а теперь стала выглядеть так:

method TestClass.Method(param1 : int, param2 : int, param3 : int) : int

Это произошло потому, что в выводимой информации все типы связаны, и работа ведется не с AST, а с типизированным деревом. Так, тип теперь получается не в виде AST (PExpr), а в виде FixedType (именно такой тип имеет свойство param.ty в строке 5).

ПРЕДУПРЕЖДЕНИЕ

В статье используются новые имена свойств и типов. Прежние имена не соответствовали стандартам .Net. Если вы используете старые версии компилятора и интеграции с VS, то код из приведенных примеров может не скомпилироваться.

Кроме того, ProjectTypeInfo не генерирует сообщения для отдельных частей partial-типа. Это происходит потому, что используются более высокоуровневые абстракции, где тип представлен одним типом (TypeBuilder). Но ничто не мешает использовать все то же свойство AstParts. Информация об AST никуда не девается. Она все так же доступна на стадии WithTypedMembers. Единственно, что изменение имеющегося AST на этой стадии уже не будет учтено компилятором. Но по-прежнему можно добавлять новые типы (в виде AST) и их члены. При этом они будут сразу же типизированы и добавлены куда нужно.

Единственное, что на фазе WithTypedMembers осталось нетипизированным – это код тел методов (и других членов). Это позволяет использовать фазу WithTypedMembers для генерации кода тел методов с использованием информации о типах.

Давайте проведем еще один эксперимент. Замените в тестовом проекте описание метода «Method» на:

        public Method(param1 : int, param2 : System.Int32, param3 = 42) : int

и скомпилируйте проект поочередно с обоими макросами. Если теперь посмотреть на описание третьего параметра метода, то оно будет отличаться.

Макрос ProjectInfo выведет его так:

Paramert 3: param3 : ?

а макрос ProjectTypeInfo так:

Paramert 3: param3 : int

Знак вопроса означает, что переменная типа свободна (тип для нее не выведен). Это происходит потому, что тип для параметра, не имеющего явного описания типа, задается компилятором как «_». Естественно, что когда метод BindType производит его связывания, то получается свободная переменная типа.

В принципе, можно усложнить макрос ProjectInfo так, чтобы он анализировал значение по умолчанию для параметра и пытался вывести его, но куда проще воспользоваться стадией WithTypedMembers и соответствующим API.

Макрос Disposable

ПРЕДУПРЕЖДЕНИЕ

Есть вполне обоснованное мнение, что паттерн проектирования IDisposable, предлагаемый компанией Microsoft к использованию, является спорным. Основным спорным моментом является нарушение этим правилом одного из эмпирических правил ООП – SRP (принцип одной ответственности). По поводу этого паттерна есть хорошая статья на http://www.codeproject.com: IDisposable: What Your Mother Never Told You About Resource Deallocation.

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

В данном разделе будет описан макрос Disposable. Этот макрос реализует паттерн проектирования IDisposable, предлагаемый к использованию компанией Microsoft (прямо в описании этого интерфейса, и еще в ряде мест, например, здесь). Особенностью этого паттерна является то, что для его реализации нужно написать (или «скопипастить») довольно много кода. Причем большая часть тех, кто копирует этот код, с трудом понимает его смысл. В итоге применения этого паттерна, класс, реализующий этот паттерн, должен реализовать интерфейс IDisposable и предоставлять (хотя это не обязательно) метод, позволяющий произвести очистку ресурсов (обычно имя этого метода Dispose, но оно может быть и другим).

Данный макрос приводится только с одной целью – продемонстрировать техники, которые можно применять при разработке макросов Nemerle.

Чтобы вам было понятно, что происходит, ниже приведена заготовка данного паттерна:

      using System;
using System.Console;

class Base : IDisposable
{
  privatemutable _disposed : bool;

  public Dispose() : void
    implements IDisposable.Dispose
  {
    Dispose(true);
    GC.SuppressFinalize(this);
  }

  protectedvirtual Dispose(disposing : bool) : void
  {
    unless (_disposed)
    {
      when (disposing)
      {
        // Освобождаем управляемые ресурсы.// Отписываемся от событий и удаляем текущий объект из разных списков.
      }
      // Очистка неуправляемых ресурсов.// Обнуление полей, ссылающихся на объемные объекты // (или всех изменяемых ссылочных полей).
      _disposed = true;
    }
  }

  // Если объект владеет неуправляемыми ресурсами, реализовать:protectedoverride Finalize() : void
  {
    Dispose(false);
  }
}

class Derived : Base, IDisposable
{
  privatemutable _disposed : bool;

  protectedoverride Dispose(disposing : bool) : void
  {
    unless (_disposed)
    {
      when (disposing)
      {
        // Освобождаем управляемые ресурсы.// Отписываемся от событий и удаляем текущий объект из разных списков.
      }
      // Очистка неуправляемых ресурсов.// Обнуление полей, ссылающихся на объемные объекты // (или всех изменяемых ссылочных полей).
      _disposed = true;
    }
    base.Dispose(disposing);
  }

  // Если объект владеет неуправляемыми ресурсами, реализовать:protectedoverride Finalize() : void
  {
    Dispose(false);
  }
}

module Program
{
  Main() : void
  {
    Derived().Dispose();
    (Derived() : IDisposable).Dispose();
    using (obj = Derived())
      ();
  }
}

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

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

Разработку подобных макросов нужно начинать с написания примера кода, который должен получиться в итоге. Места, которые должны отличаться в конкретных реализациях, можно отмечать комментариями. Например, метод Dispose из предыдущего листинга может выглядеть так:

      protected
      virtual Dispose(disposing : bool) : void
{
  unless (_disposed)
  {
    when (disposing)
    {
      // Генерируем вызов IDisposable.Dispose() у полей, чей тип      // реализует IDisposable.// Код очистки управляемых ресурсов, предоставляемый через       // параметры макроса.
    }
    // Код очистки неуправляемых ресурсов, предоставляемый через     // параметры макроса.// Обнуление изменяемых полей, определенных в классе.
    _disposed = true;
  }
}

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

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

Так как макрос должен добавлять IDisposable в список реализуемых интерфейсов класса, то макрос должен работать на фазе BeforeInheritance. Использование более поздних фаз недопустимо, так как на них нельзя расширять список реализуемых интерфейсов и/или менять базовый класс.

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

Можно просто создать два разных макроса.

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

Можно проверять, реализован ли в базовом классе метод с сигнатурой «protected virtual Dispose(disposing : bool) : void».

Можно проверять, реализует ли базовый класс интерфейс IDisposable.

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

1) это позволит продемонстрировать технику работы с типами внутри макросов;

2) это решение оставляет меньше возможностей ошибиться тем, кто будет применять макрос.

СОВЕТ

В целях закрепления материала реализуйте другие способы самостоятельно.

Чтобы проверить, реализует ли базовый класс IDisposable, требуется, чтобы макрос работал на стадии не ниже BeforeTypedMembers.

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

Генерацию кода вызова IDisposable.Dispose у объектов, на которые ссылаются поля, определенные в классе, удобнее всего производить на фазе WithTypedMembers.

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

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

Добавление IDisposable в список реализуемых классом интерфейсов можно произвести в макросе Disposable, работающем на фазе BeforeInheritance. Выбор варианта паттерна на основе анализа базового класса можно произвести в макросе Disposable, но работающем на фазе BeforeTypedMembers. А генерацию кода, вызывающего IDisposable.Dispose у объектов, на которые ссылаются поля, определенные в классе, произвести в отдельном макросе ImplementDisposeFields, который будет работать на стадии WithTypedMembers и применяться к методу. Почему к методу и как он будет активироваться? Об этом чуть позже.

Пока что добавьте в проект MacroIntroLibrary папку с именем «Disposable» и добавьте в нее три файла: Disposable_BeforeInheritance.n, Disposable_BeforeTypedMembers.n и ImplementDisposeFields.n. Их содержимое и пояснения приведены ниже.

Disposable_BeforeInheritance.n
      using Nemerle;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;

namespace MacroIntroLibrary
{
  [MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Class)]
  macro Disposable(typeBuilder : TypeBuilder, disposeManaged = null, 
disposeUnmanaged = null, disposeName = null)
  {
    _ = disposeManaged; _ = disposeUnmanaged; _ = disposeName;
    typeBuilder.AddImplementedInterface(<[ System.IDisposable ]>); // 1
  }
}

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

В строке 1 к списку реализуемых интерфейсов класса добавляется интерфейс IDisposable. Обратите внимание, что добавляется он в виде AST (т.е. в не типизированном виде). Впоследствии компилятор пройдет по списку и свяжет хранящиеся в нем выражения с реальными типами. Если задать имя частично:

<[ IDisposable ]>

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

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

Disposable_BeforeTypedMembers.n
      using Nemerle;
using Nemerle.Collections;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Compiler.Typedtree;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Linq;

namespace MacroIntroLibrary
{
  [MacroUsage(MacroPhase.BeforeTypedMembers, MacroTargets.Class)]
  macro Disposable(typeBuilder : TypeBuilder, disposeManaged = <[ () ]>, 
  /* 2 */disposeUnmanaged = <[ () ]>, disposeName = <[ Dispose ]>)
  {
    DisposableImpl.DoTransform(Macros.ImplicitCTX(), typeBuilder, 
      disposeManaged, disposeUnmanaged, disposeName)
  }
  
  module DisposableImpl
  {
    public DoTransform(typer : Typer, typeBuilder : TypeBuilder, 
disposeManaged   : PExpr, 
     disposeUnmanaged : PExpr, 
     disposeName      : PExpr) : void
    {
      Macros.DefineCTX(typer); // 3def needUnmanagedDispose = !(disposeUnmanaged is<[ () ]>); // 4def iDisposableType = <[ ttype: System.IDisposable ]>;      // 5def needOverride = typeBuilder.BaseClass.TryRequire(        // 6iDisposableType);
      def defineMember(ast) { typeBuilder.Define(ast) }                 // 7//def defineMember(ast) { _ = typeBuilder.DefineWithSource(ast) } // 8
      
      defineMember(<[ decl:         [RecordIgnore]privatemutable _disposed : bool; ]>); // 9def disposeIDisposableFields = 
Macros.NewSymbol("DisposeIDisposableFields"); // 10
      
      defineMember(
        <[ decl: [ImplementDisposeFields] // 11private $(disposeIDisposableFields : name)() : void { } ]>);
      
      def disposeImple =
        if (needOverride) // 12<[ decl:protectedoverrideDispose(disposing :bool) : void {unless (_disposed)              {when (disposing)                {// Генерируем вызовы Dispose для IDisposable-полей.$(disposeIDisposableFields : name)();// 13// Вставляем код очистки управляемых ресурсов                   // предоставляемый пользователем.$disposeManaged; // 14                }// Вставляем код очистки неуправляемых ресурсов                 // предоставляемый пользователем.   $disposeUnmanaged;// TODO: Обнуляем все изменяемые поля.base.Dispose(disposing);                _disposed = true;              }            } ]>else<[ decl: protectedvirtual Dispose(disposing : bool) : void
            {
              unless (_disposed)              {when(disposing)                {// Генерируем вызовы Dispose для IDisposable-полей.$(disposeIDisposableFields : name)();// Вставляем пользовательский код очистки управляемых ресурсов. $disposeManaged;                }// Вставляем пользовательский код очистки неуправляемых ресурсов.  $disposeUnmanaged;// TODO: Обнуляем все изменяемые поля._disposed =true;              }            } ]>;

      defineMember(disposeImple);

      when (needUnmanagedDispose) // 15
        defineMember(<[ decl:protectedoverrideFinalize() :void { Dispose(false); } ]>);unless (needOverride)
      {
        def disposeMethodName = 
          match (disposeName) // 16
          {
            | <[ $(disposeMethodName : name) ]> => disposeMethodName
            | _ => // 17
              Message.Error(disposeName.Location, "Expected simple name");
              Name("Dispose")
          };
        defineMember(<[ decl:public$(disposeMethodName : name)() :voidimplementsIDisposable.Dispose          {            Dispose(true);            GC.SuppressFinalize(this);          } ]>);
      }
    }
  }
}

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

В строке 4 значение параметра disposeUnmanaged сопоставляется с паттерном <[ () ]>. Переменная needUnmanagedDispose получает значение «true», если параметр disposeUnmanaged содержит значение, отличное от void-литерала.

ПРИМЕЧАНИЕ

Оператор «is» в Nemerle – это разновидность сопоставления с образцом. Поэтому он может проверять сложные значения.

В строке 5 используется специальный тип квази-цитаты – цитата типа. В отличие от других цитат, которые преобразуются в нетипизированное AST, данный вид цитат преобразуется в вызов, возвращающий тип, описанный в цитате. Синтаксически этот вид цитат отличается префиксом «ttype:».

ПРИМЕЧАНИЕ

Цитата типа может возвращать фиксированный тип – FixedType, переменную типа – TypeVar (в случае если задан паттерн «_»). Описание типа может содержать подстановочный символ «_» в качестве параметра типов, что приводит к созданию частично фиксированного типа. Такие типы могут сопоставляться с другими типами, причем параметры типов при этом будут игнорироваться.

В строке 6 полученное строкой выше описание типа IDisposable сопоставляется с типом базового класса. Если базовый класс реализует IDisposable (или унаследован от него, в случае класса), метод TryRequire возвращает true. Префикс Try говорит о том, что данный метод не изменяет типы и не генерирует сообщений об ошибках в случае неудачи (как это делают методы без данного префикса). Это удобно, когда нужно только проверить совместимость типов, как в нашем случае. Подобнее о работе с типами можно прочесть в четвертой части статьи «Макросы Nemerle – расширенный курс».

ПРЕДУПРЕЖДЕНИЕ

Еще раз обращаю ваше внимание на то, что использовать System.Type и все что с ним связано (например, оператор typeof()) в макросах нельзя! Для работы с типами нужно использовать исключительно типы FixedType и TypeVar (и связанный с ними API).

В строке 7 объявляется функция defineMember, с помощью которой к типу будет добавляться AST генерируемых членов класса. Данная функция просто использует метод Define. Эта функция введена только для того, чтобы продемонстрировать возможность отладки сгенерированного макросами кода. Если закомментировать строку 7 и раскомментировать строку 9, то члены в класс будут добавляться с помощью метода DefineWithSource. Это приведет к тому, что для сгенерированного AST будет сгенерирован исходный код, а компилятор ассоциирует местоположение в AST с ним. В результате будет доступна отладка по сгенерированному коду. На него же будут указывать сообщения об ошибках (в случае наличия таковых).

Использование метода DefineWithSource является хорошим приемом отладки сложных макросов.

Следующая строка (9) добавляет к классу private-поле _disposed. Поле добавляется в виде AST. Формирование AST производится с помощью квази-цитаты с префиксом «decl:». Этот вид цитаты вы уже видели выше. Он генерирует AST для типов и их членов. В разделе, посвященном квази-цитатам, будут подробно разобраны все виды цитат и особенности их применения.

Обратите внимание, что к полю _disposed добавлен атрибут RecordIgnore. Это макроатрибут из стандартной библиотеки. Он подавляет генерацию параметра для поля при использовании макроатрибута Record (автоматически формирующего конструктор для всех полей типа).

В строке 10 генерируется уникальное имя для метода, который будет содержать код вызова IDisposable.Dispose для полей, объявленных в классе. Уникальное имя нужно, чтобы никто не мог нечаянно вызвать этот метод из рукописного кода, и чтобы случайно не возникло конфликта имен. Имя, генерируемое функцией NewSymbol, формируется по шаблону: _N_строкаПереданнаяВКачествеПараметра_уникальноеЧисло. Например, для строки 10 может быть сформировано имя вроде: _N_DisposeIDisposableFields_3681 (реальное имя, полученное декомпилятором). Функция NewSymbol возвращает специальный объект типа Name. Этот объект хранит «гигиеническую» информацию. Везде, где компилятор требует имена, он ожидает именно этот тип.

ПРИМЕЧАНИЕ

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

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

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

Отдельный метод потребовался потому, что формирование кода вызова IDisposable.Dispose у вложенных объектов удобнее осуществлять на стадии WithTypedMembers, а описываемый макрос Disposable работает на предыдущих стадиях.

Кроме того, данное решение позволяет продемонстрировать одни интересный прием – передачу управления другому макросу путем генерации AST, содержащего обращение к нему. Как видите, метод, добавляемый в строке 11, помечается атрибутом «ImplementDisposeFields». Это макроатрибут, объявленный в той же макросборке. Он будет описан ниже.

Макрос ImplementDisposeFields будет вызван, когда компилятор начнет типизировать метод, на котором объявлен атрибут «ImplementDisposeFields». Так как макрос «ImplementDisposeFields» работает на фазе компиляции WithTypedMembers, он сможет использовать информацию о типах.

Еще одной особенностью данной цитаты является то, что в ней применяется выражение:

$(disposeIDisposableFields : name)

По умолчанию, в сплайсы (т.е. выражение, идущее после знака $ в цитатах) можно передавать только значения типа PExpr. Однако с помощью синтаксиса $(выражение : xxx) можно передавать в сплайсы и некоторые другие типы. О возможных значениях «xxx» и соответствующих им типах можно прочесть ниже, в разделе, посвященном квази-цитированию. Пока достаточно знать, что «name» указывает на то, что в сплайс передается значение типа Name (как раз того типа, что имеет переменная disposeIDisposableFields).

Строка 12 выбирает одну из двух квази-цитат на основании значения needOverride. Так как цитаты порождают AST, возвращаемые ими значения можно легко помещать в переменные, передавать в качестве параметров и т.п.

Если needOverride равен «true», используется квази-цитата, формирующая переопределение (override) метода, если «false», то объявляющая новый виртуальный метод. Эти методы почти идентичны, за исключением того, что у методов различаются модификаторы, и первая цитата также содержит код вызывающий метод Dispose базового класса. В разделе, посвященном квази-цитированию, будет показано, как избавиться от подобного дублирования.

ПРИМЕЧАНИЕ

В коде методов есть комментарий «TODO: Обнуляем все изменяемые поля». Это место, где было бы желательно вставить генерацию кода, обнуляющего изменяемые (mutable) ссылочные поля, объявленные в классе. Я специально не стал приводить этот код. Реализуйте его самостоятельно, чтобы закрепить материал.

Интересной особенностью цитат, описывающих метод «Dispose(disposing : bool) : void», является, то что в них присутствуют сплайсы.

В строке 13 генерируется вызов метода, имя которого находится в переменной disposeIDisposableFields (сгенерированного в строке 11). Здесь также используется сплайс «name».

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

В строке 15 уже ничего интересного нет. В ней просто добавляется метод Finalize, если needUnmanagedDispose. Это приводит к тому, что финализатор добавляется только в том случае, если пользователь макроса задал значение disposeUnmanaged, и это значение отличается от void-литерала.

В строке 16 происходит формирование имени для публичного метода. Его значение передается макросу в качестве параметра disposeName. По умолчание оно равно <[ Dispose ]>, а значит, название метода, если не задано иное, будет «Dispose».

В качестве имени метода подходят не все выражения, а только выражения, определяющие имя. Описанный выше сплайс «name», как и другие виды квази-цитат, работает в две стороны. С его помощью можно как формировать AST, так и распознавать. Таким образом, паттерн <[ $(disposeMethodName : name) ]> распознает выражение, ссылающееся на имя (объект Name). При этом с переменной disposeMethodName будет связано значение имени.

Если в параметре disposeName будет выражение, отличное от имени, выполнится строка 17, в результате чего пользователю будет выдано сообщение об ошибке, а значением переменной disposeMethodName станет Name("Dispose").

Подытожим.

Данный макрос анализирует базовый тип класса, к которому он применен, и на основании того, был ли в нем реализован интерфейс IDisposable, генерирует реализацию паттерна IDisposable для базового класса или для наследника.

Код вызова Dispose у вложенных объектов генерируется отдельно, макросом ImplementDisposeFields, который будет описан ниже. В код класса добавляется метод с уникальным именем, к которому добавляется атрибут «ImplementDisposeFields», что и приводит впоследствии к вызову этого макроса.

ImplementDisposeFields.n
      using Nemerle;
using Nemerle.Collections;
using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Compiler.Typedtree;
using Nemerle.Text;
using Nemerle.Utility;

using System;
using System.Collections.Generic;
using System.Linq;

namespace MacroIntroLibrary
{
  [MacroUsage(MacroPhase.WithTypedMembers, MacroTargets.Method)]
  macro ImplementDisposeFields(typeBuilder : TypeBuilder, 
method : MethodBuilder)
  {
    ImplementDisposeFieldsImpl.DoTransform(Macros.ImplicitCTX(), 
                                           typeBuilder, method)
  }
  
  module ImplementDisposeFieldsImpl
  {
    public DoTransform(typer : Typer, typeBuilder : TypeBuilder, 
method : MethodBuilder) : void
    {
      Macros.DefineCTX(typer);
      
      def iDisposableType = <[ ttype: System.IDisposable ]>;
      
      def isMemberTypeImplementIDisposable(member : IMember) : bool // 1
      {
        member is IField // 2
        && member.GetMemType().TryRequire(iDisposableType) // 3
      }
      
      def members = typeBuilder.GetMembers(BindingFlags.DeclaredOnly // 4| BindingFlags.Instance 
                                         | BindingFlags.Public 
                                         | BindingFlags.NonPublic)
                               .Filter(isMemberTypeImplementIDisposable); 
      
      def exprs = members.Map(m => // 5<[ (this.$(m.Name :  usesite) : System.IDisposable)?.Dispose() ]>);
      
      method.Body = <[ { ..$exprs } ]>; // 6
    }
  }
}

В строке 1 объявляется локальная функция, которая проверяет, является ли член полем (строка 2), и является ли его тип подтипом IDisposable (т.е. реализует ли он этот интерфейс IDisposable).

Строка 4 получает список членов класса. Вызвать Dispose() имеет смысл только у экземплярных полей, определенных в текущем классе, поэтому методу GetMembers передаются флаги DeclaredOnly и Instance.

Список, полученный в результате выполнения метода GetMembers, дополнительно фильтруется с помощью функции isMemberTypeImplementIDisposable.

В строке 5 производится преобразование (отображение) списка членов, тип которых совместим с IDisposable, в выражения, вызывающие у них метод IDisposable.Dispose.

Так как эти поля могут содержать null, при их вызове используется макрос «?.». В данном случае он будет предотвращать доступ по нулевому указателю.

Кроме того, в этой строке используется сплайс $(m.Name : usesite). Данный вид сплайса позволяет задать имя в виде строки (m.Name возвращает строку). Модификатор «usesite» говорит компилятору, что он должен ожидать строку и что эту строку нужно «подкрасить» в цвет места использования (контекста, в котором был вызван макрос).

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

В строке 6 тело метода, к которому применен макрос, заменяем на код вызова метода Dispose, сформированный в строке 5.

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

У вас может возникнуть вопрос: «Почему обрабатываются только поля, а не поля и автосвойства?». Это происходит потому, что автосвойства тоже формируют поля, и они входят в список членов, возвращаемый методом GetMembers.

Чтобы протестировать работу описанных выше макросов, создайте еще один тестовый консольный проект с именем «DisposableTest». В его файл Main.n поместите следующий код:

      using System;
using System.Console;
using System.IO;

using MacroIntroLibrary;

[Disposable(WriteLine("Dispose managed resources."), 
            WriteLine("Dispose unmanaged resources."), 
            Close)
]
class Base
{
}

[Record]
[Disposable]
class Derived : Base
{
  private FileStream : FileStream;
  private Str        : string;
  public Reader      : TextReader { get; privateset; }
}

module Program2
{
  Main() : void
  {
    
    def x = Derived(null, "", null);
    x.Close();
    _ = ReadLine();
  }
}

Если теперь декомпилировать решение и, с помощью декомпилятора вроде Reflector, сборку DisposableTest.exe, то вы увидите нечто вроде следующего:

      internal
      class Derived : Base
{
  // Fields
  [IgnoreField]
  privatebool _disposed;
  [CompilerGenerated, DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private TextReader _N_Reader_3702;
  private readonly FileStream FileStream;
  private readonly string Str;

  // Methodspublic Derived(FileStream fileStream, string str, TextReader reader)
  {
    this.FileStream = fileStream;
    this.Str = str;
    this.Reader = reader;
  }

  privatevoid _N_DisposeIDisposableFields_3682()
  {
    IDisposable fileStream = this.FileStream;

    if (fileStream != null)
      fileStream.Dispose();

    IDisposable disposable2 = this._N_Reader_3702;

    if (disposable2 != null)
      disposable2.Dispose();
  }

  protectedoverridevoid Dispose(bool disposing)
  {
    if (!this._disposed)
    {
      if (disposing)
        this._N_DisposeIDisposableFields_3682();

      base.Dispose(disposing);
      this._disposed = true;
    }
  }

  // Propertiespublic TextReader Reader
  {
    [CompilerGenerated]
    Get { returnthis._N_Reader_3702; }
    [CompilerGenerated]
    privateset { this._N_Reader_3702 = value; }
  }
}

Как видите, наш макрос отлично работает.

Квази-цитирование

Цитаты

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

Итак, квази-цитата – это цитата кода, которая преобразуется компилятором в тот или иной вид AST (объектную модель кода). В Nemerle поддерживается семь видов цитат (и это число может со временем увеличиться). Шесть из них имеют префиксы, а одни не имеет префикса. Цитата без префикса возвращает тип PExpr (из пространства имен Nemerle.Compiler) и описывает выражение Nemerle. Остальные префиксы используются для порождения AST верхнего уровня (типов, их членов, локальных функций, их параметров) и описания некоторых специальных случаев. Более подробно данные цитаты разобраны в разделе «Квази-цитаты с префиксом».

Все виды квази-цитат, кроме цитаты с префиксом ttype, могут как порождать AST, так и использоваться в качестве образца при сопоставлении с образцом (в операторе match). Это позволяет единообразным способом конструировать AST и производить его декомпозицию.

ПРИМЕЧАНИЕ

Возможность использования квази-цитат для декомпозиции кода является одной из мощнейших возможностей макросистемы Nemerle и выгодно отличает Nemerle от языков, поддерживающих метапрограммирование в строковом виде (таких как Ruby, Python, D).

Ниже каждый вид цитат будет рассмотрен по отдельности.

Сплайсы

Приставка «квази» в названии «квази-цитата» появилась потому, что цитаты могут быть не монолитными. В цитатах можно использовать специальные конструкции – сплайсы, которые позволяют вставить в цитату кусок внешнего AST или объект другого типа.

Есть два вида сплайсов, $x и ..$x, где «x» – это идентификатор или заключенное в скобки произвольное выражение, возвращающее вставляемое значение.

..$x отличается тем, что позволяет вставлять список вместо единичного значения. Использовать сплайс ..$x можно только внутри скобок (фигурных, квадратных или круглых). Так, следующее выражение не является допустимой цитатой:

<[ ..$x ]>

Ниже приведены примеры использования цитаты ..$x.

Генерация блока выражений:

        def expressions = [<[ def a = 1; ]>, <[ def b = 2; ]>, <[ a + b ]>];
<[ { ..$expressions } ]>

Данный пример генерирует блок кода следующего содержания:

{
  def a = 1;
  def b = 2;
  a + b
}

Генерация кортежа:

        def expressions = [<[ 42 ]><[ "String" ]><[ a + b ]>];
<[ (..$expressions) ]>

Данный пример генерирует блок кода следующего содержания:

(42, "String", a + b)
СОВЕТ

Если в предыдущем примере заменить круглые скобки квадратными, то вместо кортежа будет сгенерирован список. Однако такой список не пройдет типизацию, так как списки не должны содержать элементы разного типа.

Цитаты можно использовать и для распознавания кода. Например, поскольку вызов функции в Nemerle есть последовательно идущие друг за другом имя функции и кортеж аргументов, мы можем распознать AST вызова функции вот таким паттерном:

        match (expr : PExpr)
{
  | <[ Test(..$args) ]> => // в args список аргументов
  | _                   => ...
}
ПРИМЕЧАНИЕ

Выражение «expr : PExpr» - это уточнение типов. В Nemerle уточнять типы можно в любом месте выражения. В данном случае уточнение использовано для того, чтобы вы лучше понимали, какой тип имеет выражение «expr». Компилятор Nemerle сравнивает реальный тип выражения и тип уточнения. Если типы совпадают, компилятор никак не реагирует на это. Если типы несовместимы, но для них присутствует неявное приведение типов, то компилятор его подставляет. Если типы несовместимы и отсутствует неявное приведение типов, то компилятор выдает сообщение об ошибке.

В данном случае паттерн вводит локальную переменную args, с которой связывается список параметров. Обратите внимание, что имя функции задано фиксировано. Это означает, что данный паттерн сопоставится только с вызовом функции «Test». При этом количество аргументов вызова не важно, так как сплайс «..$args» сопоставится со списком выражений любой длины (в том числе и пустым).

ПРИМЕЧАНИЕ

Те кто пытается изучать код компилятора, зачастую, задают вопрос – что означает паттерн <[ @*(..$args) ]>? Этот паттерн разбирает оператор «*». Дело в том, что в AST Nemerle операторы отражаются как вызов обычный функции именем которой является имя оператор. Знак «@» позволяет преобразовать ключевое слов или оператор в допустимое имя Nemerle. Посмотрите как объявлялся макро-оператор «&&», в первой части «Язык Nemerle». Он так же был обвялен в префиксной форме, а перед его именем добавлен знак «@». Однако это не мешает применению данного оператора в инфиксной форме.

Сплайс $x позволяет вставить или распознать единичное выражение (PExpr). Скажем, чтобы предыдущий пример мог распознавать любые вызовы, а не только вызовы функции «Test», можно использовать в паттерне сплайс $x:

        match (expr : PExpr)
{
  | <[ $func(..$args) ]> => // сопоставляется с любым вызовом
  | _                    => ...
}

Сплайс $x имеет ряд разновидностей, позволяющих использовать выражения, тип которых отличен от PExpr. Они имеют синтаксис уточнения типа - $(«выражение» : «тип цитаты»). Поддерживаются следующие типы цитат:

Пример использования name и usesite для генерации имен можно наблюдать выше. А вот как может выглядеть их использование в паттернах (если вставить этот код внутрь макроса Disposable):

        match (disposeName)
{
  | <[ $(name : dyn) ]> => Message.Hint($"$name ($(name.GetType()))");
  | _ => ()
}

Введет при компиляции:

Main.n(7,2): warning : hint: Close (System.String)
Main.n(13,2): warning : hint: Dispose (System.String)

Того же эффекта можно достичь, заменив в примере выше dyn на usesite.

Если заменить dyn на name, то вывод изменится следующим образом:

Main.n(7,2): warning : hint: Close (Nemerle.Compiler.Parsetree.Name)
Main.n(13,2): warning : hint: Dispose (Nemerle.Compiler.Parsetree.Name)

А вот так может выглядеть применение литеральных цитат:

        def message : string = "Текстовая строка";
<[ def message : string = $(message : string) ]>

Другой пример:

        def ast : PExpr = <[ $("Строка" : string) ]>;
match (ast)
{
  | <[ $(str : string) ]> => Message.Hint($"$str ($(str.GetType()))");
  | _ => ()
}

Введет при компиляции:

Main.n(7,2): warning : hint: Строка (System.String)
ПРЕДУПРЕЖДЕНИЕ

Частой ошибкой начинающих писателей макросов является попытка подставить в сплайс выражение, чей тип отличается от PExpr, не задавая тип цитаты. К сожалению, диагностические сообщения, выдаваемые при этом, оставляют желать лучшего. Будьте аккуратны, задавая сплайсы.

Места, где необходимо использовать Name

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

Однако есть некоторые особенности которые нужно знать, чтобы не испытывать дискомфорт при написании макросов Nemerle.

В этом разделе пойдет речь об одной из таких особенностей. Почти в любом месте цитаты, когда вы вставляете сплайс, ожидается, что он будет содержать выражение типа PExpr. Однако есть места, в которых компилятор требует присутствия типа Name. Это связано с грамматикой Nemerle.

Примерами таких мест являются выражение «доступ к члену»:

obj.Member

и имя метода при его декларации:

          public MethodName() : void { }

Если потребуется создать цитату, задающую имя члена в выражении «доступ к члену» (Member Access) или имя метода при его декларации, то вместо выражения типа PExpr необходимо использовать выражение типа Name.

Так, на следующий код:

          def nameExpr : PExpr = <[ Member ]>;
_ = <[ obj.$nameExpr ]>;

компилятор выдаст сообщение об ошибке (причем не самое интуитивно понятное):

(34,14): error : each overload has an error during call:

(34,14): error : overload #1, "constructor Nemerle.Compiler.Parsetree.PExpr.Member..ctor(obj : Nemerle.Compiler.Parsetree.PExpr, member : Nemerle.Compiler.Parsetree.PExpr.Ref) : Nemerle.Compiler.Parsetree.PExpr.Member" fail because:
(34,19): error : in argument #2 (member), needed a Nemerle.Compiler.Parsetree.PExpr.Ref, got Nemerle.Compiler.Parsetree.PExpr: Nemerle.Compiler.Parsetree.PExpr is not a subtype of Nemerle.Compiler.Parsetree.PExpr.Ref [simple require]

(34,14): error : overload #2, "constructor Nemerle.Compiler.Parsetree.PExpr.Member..ctor(obj : Nemerle.Compiler.Parsetree.PExpr, member : Nemerle.Compiler.Parsetree.Splicable) : Nemerle.Compiler.Parsetree.PExpr.Member" fail because:
(34,19): error : in argument #2 (member), needed a Nemerle.Compiler.Parsetree.Splicable, got Nemerle.Compiler.Parsetree.PExpr: Nemerle.Compiler.Parsetree.PExpr is not a subtype of Nemerle.Compiler.Parsetree.Splicable [simple require]

(34,14): error : overload #3, "constructor Nemerle.Compiler.Parsetree.PExpr.Member..ctor(loc : Nemerle.Compiler.Location, obj : Nemerle.Compiler.Parsetree.PExpr, member : Nemerle.Compiler.Parsetree.Splicable) : Nemerle.Compiler.Parsetree.PExpr.Member" fail because:
(34,14): error : wrong number of parameters in call, needed 3, got 2

Есть следующие варианты исправления этой ошибки:

1. Подставлять в цитату объект типа Name, а также изменить тип цитаты на Name.

Например:

          def nameExpr = Macros.UseSiteSymbol("Member");
<[ obj.$(nameExpr : name) ]>

При этом имя можно сформировать по-разному. Можно воспользоваться функциями UseSiteSymbol или NewSymbol. Можно описать имя в квази-цитате и получить к нему доступ через поле «name»:

          def nameExpr = <[ Member ]>.name;
<[ obj.$(nameExpr : name) ]>
ПРИМЕЧАНИЕ

PExpr не имеет поля «name». Данное поле принадлежит к подтипу PExpr.Ref, который описывает ссылку на имя. Но реально квази-цитата генерирует не PExpr, а конкретные виды PExpr. Так что в вышеприведенном выражении тип квази-цитаты как раз и будет PExpr.Ref. А это позволяет обратиться к его членам. При этом даже работает IntelliSence!

В общем, неважно, как вы получите объект типа Name. Важно, что нужно использовать цитату типа name.

2. Если у вас есть строка, то использовать ее в качестве имени можно с помощью цитат типа usesite, dyn или global. Например:

          <[ obj.$("Member" : usesite) ]>

Данный пример аналогичен по функциональности примеру, где использовалась функция Macros.UseSiteSymbol.

3. Можно описать цитату содержащую только одно имя:

          def nameExpr : PExpr = if (condition) <[ Member1 ]> else <[ Member2 ]>;
_ = <[ obj.$nameExpr ]>;

В отличии от исходного примера – это «пойдет, так как цитаты будут иметь тип PExpr.Ref (ссылка на имя), а он допустим в качестве имени члена в выражении доступа к члену.

4. Сформировать объект типа Name вручную. Это не самый лучший способ, так как при этом нужно хорошо понимать принципы гигиены макросов.

Создание квалифицированного имени

Иногда нужно получить не простое имя, как например «String», а квалифицированное, как например, имя типа «Nemerle.Collections.Hashtable». В Nemerle нет типа цитаты, который позволял бы получить такое имя из строки. Поэтому для преобразования строки в PExpr, описывающий квалифицированное имя, нужно воспользоваться функцией PExpr.FromQualifiedIdentifier:

          def str = "Nemerle.Collections.Hashtable";
def expr = PExpr.FromQualifiedIdentifier(typer.Manager, str);
Message.Hint($"'$expr' ($(expr.GetType()))");

Этот код выведет:

... 'Nemerle.Collections.Hashtable' (Nemerle.Compiler.Parsetree.PExpr+Member)

Однако этот метод окажется бесполезным, если строка будет содержать более сложное выражение.

Например, довольно частой задачей является формирование выражения, описывающего тип (для возвращаемого значения или вызова конструктора). При этом бывает, что описание типа уже присутствует в виде строки. Для преобразования его в выражение можно воспользоваться парсером Nemerle. Он доступен в виде функции. Вот как это может выглядеть:

          def typeAsString = "Hashtable[int, List[string]]";
def parsedType   = MainParser.ParseExpr(typer.Env, typeAsString, true);
Message.Hint($"'$parsedType' ($(parsedType.GetType()))");

Этот код выведет:

... 'Hashtable[int, List[string]]' (Nemerle.Compiler.Parsetree.PExpr+Indexer)

Ссылка на тип

Иногда вместо строкового представления вы может располагать ссылкой на тип. В этом случае не имеет смысла преобразовывать его в строку и потом парсить. В таком случае лучше воспользоваться цитатой типа «type». Вот как это может выглядеть:

          def typedType = typeBuilder.GetMemType();
def expr = <[ $(typedType : typed) ]>;
Message.Hint($"'$expr' ($(expr.GetType()))");

Данный код выведет:

... 'Base' (Nemerle.Compiler.Parsetree.PExpr+TypedType)
... 'Derived' (Nemerle.Compiler.Parsetree.PExpr+TypedType)

Метод GetMemType возвращает так называемый невоплощенный тип, т.е. тип, чьи параметры типов связаны с параметрами типов. Его можно использовать только в рамках членов этого же типа. Если вам нужен тип со свободными параметрами типов или с параметрами типов, связанными с другими типами, то нужно воспользоваться функцией GetFreshType:

          def typedType = typeBuilder.GetFreshType();
unless (typedType.args.IsEmpty)
  _ = typedType.args.Head.Unify(typer.InternalType.Int32);
def expr = <[ $(typedType : typed) ]>;
Message.Hint($"'$expr' ($(expr.GetType()))");

Данный код выведет:

... 'Base' (Nemerle.Compiler.Parsetree.PExpr+TypedType)
... 'Derived[int]' (Nemerle.Compiler.Parsetree.PExpr+TypedType)

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

Поясню, что происходит в этом примере. Функция GetFreshType возвращает значение типа FixedType.Class. Это тип, описывающий пользовательские типы (классы и структуры). У него есть два основных поля: tycon типа TypeInfo из пространства имен Nemerle.Compiler (лучше использовать для доступа к нему свойство TypeInfo) и поле args типа list[TypeVar]. В поле args хранятся переменные типов. Выражение typedType.args.Head возвращает переменную типов, соответствующую первому параметру типов.

Переменные типов можно унифицировать с другими типами. Если унифицировать ее с некоторым FixedType, то в случае успеха значением переменной типа станет значение фиксированного типа. Стало быть, выражение:

typedType.args.Head.Unify(typer.InternalType.Int32);

делает параметр первый типов типа равным Int32. Поле InternalType, имеющееся у многих объектов компилятора, ссылается на тип InternalTypeClass, в котором объявлено множество ссылок на часто используемые типы. Вместо его использования можно использовать квази-цитату типа «ttype», как уже было показано в примере Disposable.

Более подробно о действии функции Unify (а так же Require и других) вы можете прочесть в четвертой части статьи «Макросы Nemerle – расширенный курс».

Квази-цитаты с префиксом

Как уже говорилось раньше, квази-цитата без префикса описывает выражение (PExpr). Если нужно получить AST, отличное от выражения, то нужно использовать специальные префиксы. Вы уже видели использование цитат с префиксом «decl» и «ttype» в примерах, приведенных выше. Настала пора познакомиться со всеми видами префиксов.

Ниже приведен список поддерживаемых квази-цитатами Nemerle префиксов и их описание:

Синтаксис всех видов квази-цитат будет разобран в отдельном разделе, посвященном описанию цитат. Пока же сосредоточимся на практическом использовании квази-цитат.

Цитаты с префиксом «decl»

С помощью квази-цитаты с префиксом «decl» можно конструировать и распознавать довольно много видов AST.

Квази-цитаты с префиксом «decl» не могут конструировать или распознавать вхождения вариантов (так как их грамматика конфликтует с грамматикой вхождений перечислений), пространства имен и конструкцию «using» (так как эти конструкции не отображаются в AST Nemerle).

Общий формат квази-цитаты с префиксом «decl»:

<[ decl: «атрибуты и модификаторы» «описание типа или члена» ]>

«Описание типа или члена» разное для каждого вида цитируемых сущностей, и может поддерживать синтаксические расширения, а «атрибуты и модификаторы» всегда описывают модификаторы (public, private, partial, abstract, ...) и пользовательские атрибуты (в том числе и макро-атрибуты).

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

ПРЕДУПРЕЖДЕНИЕ

Опуская модификаторы и атрибуты в квази-цитате, используемой как паттерн, нужно быть осторожным, так как при этом образец будет сопоставляться только с AST, в котором также не заданы модификаторы и атрибуты.

Чтобы распознать AST с любым набором модификаторов и атрибутов, нужно использовать сплайс типа «..$x». Обратите внимание, что этот сплайс будет одновременно распознавать и модификаторы, и атрибуты. Таким образом, конструкция вида:

          <[ 
          decl
          : [Record] ..$attrs class MyClass { } ]>

недопустима. Если есть сплайс типа ..$x, то модификаторы (public, abstract и т.п.), заданные в коде цитаты, будут проигнорированы (а в следующих версиях компилятора, возможно, будет выдаваться сообщение об ошибке):

          def attrs = AttributesAndModifiers(NemerleAttributes.Private, []);
<[ decl: ..$attrs public class MyClass { } ]> // public будет проигнорирован

Так что не задавайте модификаторы и атрибуты одновременно и в виде сплайса, и в виде кода.

Если вам нужно программно задать значения модификаторов или атрибутов, используйте сплайс ..$x, идущий в самом начале цитаты, и указывайте модификаторы доступа в первом параметре конструктора класса AttributesAndModifiers.

ПРИМЕЧАНИЕ

Класс AttributesAndModifiers описывает модификаторы доступа и пользовательские атрибуты. Модификаторы задаются в виде битовой маски, с помощью перечисления NemerleModifiers.

Чтобы продемонстрировать, как задавать модификаторы доступа программно, давайте перепишем часть реализации макроса Disposable, отвечающую за добавление метода «Dispose(disposing : bool) : void». В исходной реализации реализация попросту дублировалась.

          def modifiers = // 1if (needOverride) NemerleModifiers.Protected | NemerleModifiers.Override
  else              NemerleModifiers.Protected | NemerleModifiers.Virtual;
def attributesAndModifiers = AttributesAndModifiers(modifiers, []); // 2def baseCall = if (needOverride) <[ base.Dispose(disposing); ]>else<[ () ]>;
                 
defineMember(
    <[ decl:..$attributesAndModifiers Dispose(disposing : bool) : void{unless (_disposed) {when(disposing)          {// Генерируем вызовы Dispose для IDisposable-полей. $(disposeIDisposableFields : name)();// Вставляем код очистки управляемых ресурсов предоставляемый пользователем. $disposeManaged; }// Вставляем код очистки неуправляемых ресурсов предоставляемый пользователем.$disposeUnmanaged;// TODO: Обнуляем все изменяемые поля. $baseCall; _disposed = true;}      } ]>);

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

Строка 1 вышеприведенного примера формирует модификаторы метода. Модификаторы описываются перечислением (enum) NemerleModifiers. Данное перечисление помечено атрибутом System.Flags, что позволяет использовать его для задания битовой маски (сочетания флагов). Вот описание этого перечисления:

[System.Flags]
publicenum NemerleModifiers
{
  | None            = 0x00000
  | Public          = 0x00001
  | Private         = 0x00002
  | New             = 0x00004
  | Protected       = 0x00008
  | Abstract        = 0x00010
  | Virtual         = 0x00020
  | Sealed          = 0x00040
  | Static          = 0x00080
  | Mutable         = 0x00100
  | Internal        = 0x00200
  | Override        = 0x00400
  | Struct          = 0x01000
  | Macro           = 0x02000
  | Volatile        = 0x04000
  | SpecialName     = 0x08000
  | Partial         = 0x10000
  | Extern          = 0x20000
  /// field is immutable, but compiler overrides it and can assign something
  | CompilerMutable = 0x40000 

  | VirtualityModifiers = New | Abstract | Virtual | Override
  | OverrideModifiers   = Abstract | Virtual | Override
  | AccessModifiers     = Public | Private | Protected | Internal
}

Строка 2 формирует объект типа AttributesAndModifiers описывающий модификаторы и пользовательские атрибуты. Первый параметр конструктора задает модификаторы, а второй – список пользовательских атрибутов.

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

Например, в стандартной библиотеке макросов есть макрос RecordIgnore, который позволяет указывать, что некоторые из полей или свойств нужно игнорировать (не добавлять для них параметры в генерируемом макросом Record конструкторе). Макрос RecordIgnore работает на стадии BeforeInheritance. Он добавляет помеченные им члены в список (точнее, хэш-таблицу), который в свою очередь помещается в именованное пользовательское свойство «RecordIgnored-MemberIgnoreMapKey».

Класс TypeBuilder имеет индексируемое свойство UserData типа System.Collections.IDictionary:

          public UserData : SC.IDictionary
    {
      get
      {
        when (_userData == null)
          _userData = ListDictionary();

        _userData
      }
    }

Это свойство позволяет сохранять информацию, требуемую макросам. Так как TypeBuilder-ы пересоздаются при изменениях в проекте, затрагивающих AST верхнего уровня, информацию из свойства UserData не нужно удалять. А так TypeBuilder-ы не пересоздаются между фазами компиляции, через их свойство UserData очень удобно передавать информацию между макросами и/или одним и тем же макросом, работающим на разных фазах компиляции.

Вот как выглядит реализация макроса RecordIgnore из стандартной макробиблиотеки:

[MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Field)]
macro RecordIgnore(ty : TypeBuilder, fld : ParsedField)
{
  MacrosHelper.MarkIgnored(ty, fld);
}

[MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Property)]
macro RecordIgnore(ty : TypeBuilder, property : ParsedProperty)
{
  MacrosHelper.MarkIgnored (ty, property);
}
...
private IgnoreMapKey = "RecordIgnored-MemberIgnoreMapKey"; 

public MarkIgnored (ty : TypeBuilder, member : PT.ClassMember) : void
{
  ...
  
  def saveMarkInUserData(ty : TypeBuilder, member : PT.ClassMember) : void
  {
    mutable ignoreMap = 
ty.UserData[IgnoreMapKey] :> SCG.Dictionary[PT.ClassMember, byte];
  
    when (ignoreMap == null)
    {
      ignoreMap = SCG.Dictionary();
      ty.UserData[IgnoreMapKey] = ignoreMap;
    }

    ignoreMap[member] = 1; // хэш-таблица исползуется в роли набора (Set)
  }
  
  saveMarkInUserData(ty, member);
}
СОВЕТ

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

В дальнейшем значения, запомненные UserData, используются в коде макроса Record для фильтрации списка полей:

          ...
          def ignoreMap = tb.UserData[IgnoreMapKey] 
  :> SCG.Dictionary[PT.ClassMember, byte];
def isIgnored(member : PT.ClassMember) : bool
{
  ignoreMap != null && ignoreMap.ContainsKey(member);
}
// используем функцию isIgnored для фильтрации полей...

Добавление типов

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

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

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

ПРЕДУПРЕЖДЕНИЕ

Не пытайтесь добавить вложенный тип с помощью метода TypeBuilder.Define. Такой код пройдет компиляцию, но потерпит неудачу при выполнении.

Вот пример добавления вложенного типа:

          def members = формируем список AST членов...
def nested = tb.DefineNestedType(<[ decl:
  [Nemerle.Core.Record]
  publicclass MyNestedClass
  {
    ..$members
  } ]>);
  
nested.Compile();

Как видите, все довольно просто. Главное, не забыть в конце работы с типом вызвать Compile. Но сделать это нужно после завершения работы с типом.

Чтобы добавить тип в пространство имен, нужно воспользоваться методом GlobalEnv.Define.

Класс GlobalEnv определяет так называемое окружение. В него входит текущее пространство имен, открытые пространства имен и открытые типы. Чтобы поместить тип в некоторое пространство имен, нужно получить объект GlobalEnv, в котором оно является текущим.

Объект Typer, который неявно передается в любой макрос, имеет поле Env. В нем содержится экземпляр GlobalEnv, описывающий окружение, в котором был использован текущий макрос. Его можно использовать для объявления типа в том же пространстве имен. Следующий пример макроса создает класс в том же пространстве имен, в котором будет применяться макрос:

[MacroUsage(MacroPhase.BeforeInheritance, MacroTargets.Assembly)]
macro AddClass()
{
  Macros.DefineCTX(typer);
  def builder = typer.Env.Define(<[ decl:
      publicclass MyType
      {
        public Test() : void {  }
      } ]>);
          
  builder.Compile();
}

Вот как можно использовать данный макрос:

          namespace MyNamespace
{
  [assembly: AddClass]
}

namespace OtherNamespace
{
  [assembly: AddClass]
}

module Program
{
  Main() : void
  {

    MyNamespace.MyType().Test();
    OtherNamespace.MyType().Test();
  }
}

Если нужно определить тип в заранее известном пространстве имен, нужно воспользоваться так называемым корневым (Core) окружением, в котором текущим пространством имен является корневое (безымянное) пространство имен. Добраться до него можно следующим образом:

typer.Manager.CoreEnv

Окружение позволяет «войти» во вложенное пространство имен. Например, если в объекте окружения текущем пространством имен является «X», то из него можно войти, например, в пространство имен «X.Y», но нельзя войти в «Z». Из основного пространства имен можно войти в любое пространство имен.

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

          macro AddClassInsideNamespace(namespaceName : PExpr)
{
  def typer = Macros.ImplicitCTX();
      
  match (Util.QidOfExpr(namespaceName))
  {
    | Some((qualifiedIdentifier, _)) =>
      def env = typer.Manager.CoreEnv.EnterIntoNamespace(qualifiedIdentifier);
      def builder = env.Define(<[ decl:publicclassMyType          {publicTest() : void {  }} ]>);
          
      builder.Compile();

    | None => Message.FatalError(namespaceName.Location, 
                "expected qualified identifier");
  }
}

Функция QidOfExpr принимает выражение (PExpr) и возвращает тип «option[list[string] * Name]», т.е. кортеж, состоящий из списка строк и описывающий квалифицированный идентификатор и имя, запакованные в тип option[T]. Если во входном выражении содержится некорректный квалифицированный идентификатор (т.е. не имя, разделенное точками), QidOfExpr возвращает None(). В случае успеха первая часть кортежа содержит список строк, описывающий квалифицированный идентификатор. Так, для выражения «A.B.C» будет возвращен список «["A", "B", "C"]». Имя, идущее вторым элементом кортежа, содержит последнее простое имя в выражении (т.е. "C" для предыдущего примера). Так как имя в Nemerle содержит в себе ссылку на окружение, в котором оно объявлено, им можно воспользоваться для разрешения имен. Но в данном примере имя не используется.

Вот как можно применить данный макрос:

          namespace MyNamespace
{
  [assembly: AddClassInsideNamespace(A.B.C)]
  [assembly: AddClassInsideNamespace(X.Y.Z)]
}

module Program
{
  Main() : void
  {
    A.B.C.MyType().Test();
    X.Y.Z.MyType().Test();
  }
}

Добавление членов в добавленный тип

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

1. Сгенерировать список членов программно (например, с помощью квази-цитат «decl») и подставить его с помощью квази-цитаты вида ..$x:

          def members : list[PExpr] = ...;
def builder = env.Define(<[decl:publicclassMyType  {    ..$members
  } ]>);
...

2. Можно добавлять члены с помощью метода Define объекта TypeBuilder, возвращаемого GlobalEnv.Define или TypeBuilder.DefineNestedType:

          def builder = env.Define(<[decl:publicclassMyType  {  } ]>);
builder.Define(<[ decl: mutablefield : int; ]>);
builder.Compile();

Получение дополнительной информации о макросах

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

Отладка макросов

Так как макросы являются обычными программами на языке Nemerle, для их отладки можно применять те же средства, что и для отладки обычных .Net-приложений. Единственное отличие макросов от, например, консольных приложений заключается в том, что они исполняются в процессе компилятора или IDE. Соответственно, вам придется отлаживать приложение ncc32.exe (или ncc64.exe в зависимости от битности ОС), по умолчанию располагающееся в каталоге «%ProgramFiles%\Nemerle\Net-4.0») или devenv.exe (VS 2010), по умолчанию располагающееся в каталоге «%ProgramFiles%\Microsoft Visual Studio 10.0\Common7\IDE».

Что конкретно отлаживать (devenv.exe или ncc.exe), зависит от того, в каком окружении вы хотите отлаживать макросы.

Для отладки данных исполняемых модулей в VS нужно прописать путь к ним в свойствах макропроекта, в разделе «Debug\Start Program». Кроме того, для компилятора (ncc.exe) нужно прописать опции командной строки, которые заставят компилятор компилировать проект, использующий ваши макросы. Проще всего добиться этого, если создать файл, содержащий опции компиляции, и указать компилятору опцию командной строки «-from-file:имя-этого-файла». Например, чтобы отлаживать компиляцию проекта DisposableTest, можно создать файл CompilerOptions.txt следующего содержания:

/no-color 
/no-stdlib 
/greedy-references:- 
/define:DEBUG;TRACE 
/target:exe 
/debug+ 
/project-path:C:\Nemerle-Articles\Nemerle-macros-intro\Projects\MacroIntro\DisposableTest\DisposableTest.nproj 
/root-namespace:DisposableTest

Main.n
Properties\AssemblyInfo.n
 
/ref:C:\Nemerle-Articles\Nemerle-macros-intro\Projects\MacroIntro\MacroIntroLibrary\bin\Debug\MacroIntroLibrary.dll 
/ref:"C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\mscorlib.dll" 
/ref:"C:\Program Files\Nemerle\Net-4.0\Nemerle.dll" 
/ref:"C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Core.dll" 
/ref:"C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.dll" 
/ref:"C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Xml.dll" 
/ref:"C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\System.Xml.Linq.dll" 
/macros:"C:\Program Files\Nemerle\Net-4.0\Nemerle.Linq.dll" 
/out:obj\Debug\DisposableTest.exe
СОВЕТ

Содержимое было получено методом copy-paste из окна «Output» после компиляции проекта «DisposableTest».

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

СОВЕТ

Если собрать компилятор Nemerle и интеграцию из исподников, то можно будет отлаживать не только код ваших макросов, но и код компилятора. О том, как собрать код Nemerle из исходников, написано здесь.

Если такой путь вам кажется сложным, то можно поступить очень просто – добавьте в любом месте макроса строчку кода:

assert2(false); 

и запустите перекомпиляцию решения, содержащего проект макроса и отладочный проект. При этом появится диалог «Assert», см. рисунок 9. Если нажать в нем кнопку «Повторить» (Retry), то появится стандартное диалоговое окно подключения отладчика к процессу. Пробравшись через ряд диалоговых окон (при работе под Windows Vista их больше), вы откроете новую копию VS, в которой сможете производить отладку макросов.


Рисунок 9. Диалог «Assert» позволяет начать отладку макроса.

ПРИМЕЧАНИЕ

Диалог «Assert» будет появляться только в Debug-версии макросов, и только во время его компиляции. При работе под управлением IDE диалог появляться не будет, так как в IDE выдача диалогов отключена, чтобы не раздражать пользователя.

Для отладки макроса в режиме IDE вам придется запустить одну VS из-под другой. Для этого вам необходимо прописать в качестве отлаживаемого приложения путь к devenv.exe (соответствующей версии), запустить вторую копию VS под отладку, поставить точки останова в нужных местах макросов и открыть проект, в котором используются ваши макросы. При этом сработают точки останова, и вы сможете начать пошаговую отладку.

При пошаговой отладке вы можете видеть фрагменты кода в окнах «Watch» и «Locals» и просматривать их структуру. Также можно использовать окно «Immediate» для экспериментов и вывода больших фрагментов кода (это удобно, так как в «Watch» и «Locals» не отображаются концы строк).

Если требуется отладка сгенерированного макросом кода, то для добавления членов и типов вместо метода TypeBuilder.Define можно использовать метод DefineWithSource. Это приведет к генерации исходного кода и ассоциации отладочной информации с ним. Естественно, что отлаживать при этом нужно не макрос, а приложение, в котором оно используется.

Конечно же, для отладки макросов можно использовать и логирование. Но рассказывать тут не о чем, так как методы отладки логированием одинаковы для приложений любого типа. Ну, и как было показано выше, можно выводить информацию в окно «Output» VS с помощью Message.Hint(). Также для логирования можно использовать макрос Log из стандартной библиотеки Nemerle или любую библиотеку логирования .Net.

Главное при отладке макросов, как и при их разработке, помнить, что макрос – не часть целевой программы, а модуль расширения к компилятору и IDE!

Заключение

Данная часть получилась весьма немаленькой, но я надеюсь, что она поможет вам в нелегком деле разработки собственных нетривиальных макросов. В следующих частях «Язык Nemerle» будет приведена справочная информация, снабженная краткими и простыми примерами. Данная же часть предназначена для того, чтобы объяснить и показать на что способны макросы и как их готовить.

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

Ссылки


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