Работа с типами из макросов Nemerle
От: VladD2 Российская Империя www.nemerle.org
Дата: 11.04.11 16:33
Оценка: 204 (9)
#Имя: FAQ.nemerle.TypeInMacros
Здравствуйте, Аноним, Вы писали:

Данная мини-статья родилась в попытке ответь на вот это
Автор:
Дата: 11.04.11
сообщение. Но в итоге я понял, что получилась отдельная статья. Посему публикую ее драфт в виде отдельной темы.

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

Попробую в двух словах его описать.

Описание исходного представления кода в теле метода

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

Тело метода состоит из нетипизированного AST. Этот термин означает, что мы имеем некоторую объектную модель кода (эдакий DOM кода) которая описывает исключительно набор синтаксических конструкций. Нетипизированное АСТ выражений представляется вариантным типом — PExpr.
Скажем если тело метода (в текстовой форме) состоит из кода:
a + b

то оно будет преобразовано в следующий АСТ (упрощенно):
PExpr.Call("+", PExpr.Ref("a"), PExpr.Ref("a"))

Это похоже на CodeDom, если вы с ним знакомы.
В немерле, для представления такого АСТ внутри макросов используется квази-цитирование. Так что вместо приведенного выше "объектного" жутика можно написать просто:
<[ a + b ]>


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

В AST при этом будет примерно следующее:
PExpr.MacroCall("MyMacro", [PExpr.Call("+", PExpr.Ref("a"), PExpr.Ref("a")])

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

Сам макрос "MyMacro" может выглядеть следующим образом:
macro MyMacro(expr)
{
  expr
}

Это макрос — пустышка. Он ничего не делает. Просто передает свой единственный аргумент в качестве своего возвращаемого значения. Он не имеет смысла и нужен только для демонстрации процесса типизации.

Типизация тела метода

Типизация происходит следующим образом... Корневая ветка АСТ передается в функцию типизации. Если это ветка имеет тип PExpr.MacroCall, то ищется соответствующих макрос, которому передаются параметры PExpr.MacroCall().

Обратите внимание, что параметры макросу передаются в нетипизированном виде, то есть — это такое же АСТ в котором нет ни единого типа (и вообще ничего кроме информации о структуре кода).

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

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

Когда функция типизации встречает ветку отличную от PExpr.MacroCall, производится попытка вычислить типы этой ветки. В процессе вычисления типов формируется новое дерево, так называемое типизированное AST. Типизированное АСТ представляется вариантным типом TExpr. Для TExpr не применимо квази-цитирование, но зато TExpr содержит информацию о типах.

Проблема получения типов внутри макросов

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

Да, есть. Мы можем обратиться к API компилятора, а именно к типизатору, и попросить его типизировать параметры нашего макроса. Скажем в случае с макросом MyMacro мы можем поступить следующим образом:
macro MyMacro(expr)
{
  def typer = Macros.ImplicitCTX(); // получаем ссылку на типизатор - часть API компилятора.
  def typedExpr = typer.TypeExpr(expr); // производим типизацию выражения полученного в качестве аргумента
  
  <[ $(typedExpr : typed) ]> // возвращаем PExpr в который запаковано типизированное выражение
  // тоже самое можно было бы сделать без квази-цитирования так:
  // PExpr.Typed(typedExpr)
}

Детали описаны в комментариях.
Данный код опять же ничего нового не привносит, но теперь мы можем:
1. Проанализировать типизированное АСТ находящееся в переменной typedExpr. Это можно сделать с помощью паттерн-матчинга.
2. Анализируя нетипизированное АСТ (находящееся в параметре expr) обращаться к свойству TypedObject. В нем, для большинства выражений, находится ссылка на соответствующую ветку TExpr. Это позволяет анализируя нетипизированное АСТ (которое сохраняет всю информацию о структуре кода написанного программистом) можно получать информацию о типах.

Информация о типах доступна в форме типов TypeVar или FixedType. По сути FixedType — это наследник TypeVar, так что можно говорить, что мы всегда имеем дело с TypeVar. Вот с ними мы и можем производить сравнение, сопоставление и т.п. Но так как типы имеют не очень простые отношения (есть подтипы и не выведенные типы), то для работы с типами нужно использовать более хитрый механизм — унификацию, точнее методы Require и Unify.

Отложенная типизация (или описание алгоритма вывода типов)

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

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

Иногда типизатору не важно какой тип у TExpr-ветки. Но иногда от этого зависит какой подтип TExpr нужно создать. Другими словами без информации о типах появляется неоднозначность. Пример такой неоднозначности — вызов перегруженного метода. Без информации о типах параметров нельзя сказать, что за перегрузка должна быть вызвана. Если компилятор встречает такую ситуацию, то он создает некий объект, например, объект хранящий информацию о всех возможных перегрузках. Этот объект помещается в очередь отложенной типизации, а типизатор продолжает линейное сканирование нетипизированного АСТ с параллельным раскрытием макросов.

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

Влияние отложенной типизации на работу с типами внутри макросов

Знания из предыдущего раздела не нужны писателю макросов на практике. Я описал алгоритм вывода типов только для того, чтобы вы смогли проще понять одну вещь. Вызов typer.TypeExpr(expr) не гарантирует вывода типов. Часто встречающейся ошибкой при разработке макросов бывает попытка использовать информацию о типах непосредственно за вызовом typer.TypeExpr().

Что же делать, если нужна информация о типах?

У вас есть следующие пути:
1. Использовать метод typer.DelayMacro():
DelayMacro(resolve : bool -> option [PExpr], expected : TypeVar = null) : PExpr

2. Использовать метод typer.TransformWhenAllTypesWouldBeInfered():
TransformWhenAllTypesWouldBeInfered(
  transform  : PExpr * TExpr -> PExpr,
  tExpr      : TExpr, 
  expr       : PExpr = null
) : PExpr

3. Вызвать процесс типизации тела метода из макро-атрибута работающего на стадии WithTipedMembers предварительно запомнив нетипизированное АСТ тела метода.

DelayMacro и TransformWhenAllTypesWouldBeInfered возвращают PExpr который можно вернуть из макроса или встроить в код генерируемый макросом. Обе функции принимают функции которые будут вызваны тогда когда станут известны необходимые типы.

Разница между DelayMacro и TransformWhenAllTypesWouldBeInfered заключается в том, что DelayMacro позволяет дождаться когда типизатор введет один тип, конкретный тип (или несколько типов), а TransformWhenAllTypesWouldBeInfered позволяет дождаться момента когда все типы типизированном АСТ (параметр tExpr) будут выведены. Если в процессе вывода типов внутри tExpr взникнет ошибка, то функция transform вообще не будет выведена. Кроме того transform будет вызван ровно один раз.

Принцип работы DelayMacro

Напротив, resolve (из DelayMacro) будет вызываться многократно. По сути DelayMacro формирует еще один объект отложенной типизации который как и прочие объекты отложенной типизации помещаются в соответствующую очередь. На каждой итерации разбора этой очереди будет вызваться resolve. Реализация resolve должна проверять необходимые для ее работы типы, и если это возможно, произвести генерацию конечного АСТ (PExpr). Если resolve может сгенерировать конечное АСТ, то он должен сгенерировать его, обернуть в Some() и возвратить в качестве своего результата. Если не может, то он должен возвратить None(). Кроме того resolve получает bool-параметр который позволяет узнать не является ли данный вызов последним. Если это последний вызов, то это последний шанс сгенерировать АСТ. Обычно реализация resolve должна проверять этот параметр и если он не может сгенерировать внятного кода, то она должна хотя бы выдать осмысленное сообщение об ошибке.

Чтобы лучше понять как работает этот механизм. Рассмотрим реализацию макроса lock (аналога стэйтмента lock из C#):
  macro @lock (lockOnExpr, body)
  syntax ("lock", "(", lockOnExpr, ")", body)
  {
    def typer = Macros.ImplicitCTX();
    def lockOnTExpr = typer.TypeExpr(lockOnExpr);

    typer.DelayMacro(lastTry =>
      match (lockOnTExpr.Type.Hint)
      {
        | Some(Class(typeInfo, _)) when typeInfo.IsValueType =>
          when (lastTry)
            Message.Error (lockOnExpr.Location,
              $"`$typeInfo' is not a reference type as required by the lock expression");
          None()

        | None =>
          when (lastTry)
            Message.Error (lockOnExpr.Location,
              "compiler was unable to analyze type of locked object, but it "
              "must verify that it is reference type");
          None()

        | _ =>
          def result =
            <[
              def toLock = $(lockOnTExpr : typed);
              System.Threading.Monitor.Enter(toLock);
              try { $body }
              finally { System.Threading.Monitor.Exit(toLock); }
            ]>;

          Some(result)
      }
    );
  }


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

Нам нужно понять что за тип имеет выражение lockOnExpr и выдать сообщение об ошибке если это тип-значение. Для этого мы типизируем выражение:
    def lockOnTExpr = typer.TypeExpr(lockOnExpr);

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

Для анализа типа используется выражение lockOnTExpr.Type.Hint.

lockOnTExpr.Type — это свойство возвращающее экземпляр TypeVar. Его свойство Hint возвращает экземпляр FixedType в который бы мог бы быть преобразован исходный TypeVar, если бы мы в данный момент зафиксировали бы тип, т.е. вызвали бы метод Fix().

Обратите внимание, что мы не можем просто вызвать lockOnTExpr.Type.Fix(), так как если lockOnTExpr.Type не содержит ограничений на тип мы получим в итоге тип "object" который и станет значением lockOnTExpr.Type. Типизатор не сможет производитель дальнейший вывод типов, что попросту говоря испортит всю компиляцию.

Свойство Hint же возвращает тоже значение что и метод Fix(), но при этом не производит фиксацию, т.е. значение свойства Type, при том, остается неизменным и типизатор может производить дальнейший вывод типов.

Принцип работы TransformWhenAllTypesWouldBeInfered

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

Использование TransformWhenAllTypesWouldBeInfered выгодно тогда когда вам требуется проанализировать содержимое всего выражения и при этом анализировать типы всех подвыражений. Примером такой задачи может является макрос генерирующий деревья выражений (Expression tree) по коду немерла. Другим примером может являться генерация кода на некотором, дуругом, языке. Например, на Java Script.

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

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

Использование ручной типизации тела метода

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

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


Принцип этого подхода очень прост:
1. Берем метод.
2. Запоминаем нетипизированное АСТ его тела.
3. Создаем типизатор способный типизировать тело метода (передаем ему метод в качестве параметра).
4. Вызываем у типизатора метод RunFullTyping.
5. Как и в случае TransformWhenAllTypesWouldBeInfered анализируем АСТ (хотите типизированное, хотите нетипизированное) и выполняем некоторые действия.

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

Это позволяет типизировать таким образом тела сразу несколько методов и анализировать их совместно.

Далее вы должны восстановить тело метода поместив в него нетипизированное АСТ. Это необходимо, так как компилятор не знает о наших проделках и будет ожидать в теле метода нетипзированное АСТ. Если целью типизации тела метода был всего лишь его анализ, т.е. его не требуется изменять, то вы можете поместить в тело метода обертку над типизированным АСТ:
match (meth.Header.body)
{
  | FunBody.Typed(typedBody) => meth.Header.body = FunBody.Parsed(PExpr.Typed(typedBody));
  | _ => assert(false);
}

Здесь
Автор: VladD2
Дата: 08.04.11
описан пример использования данного подхода.

ЗЫ

Надеюсь, данного описания достаточно чтобы понять, то как можно работать с типами и Nemerle.

11.04.11 20:41: Ветка выделена из темы Работа с типами из макросов Nemerle
Автор:
Дата: 10.04.11
— VladD2
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.