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

Макросы Nemerle – расширенный курс

Часть 4

Автор: Владислав Юрьевич Чистяков
The RSDN Group

Источник: RSDN Magazine #1-2009
Опубликовано: 03.09.2009
Исправлено: 10.12.2016
Версия текста: 1.0
Макросы и типизация
Почему получение информации о типах – это сложно?
Как работает система вывода типов?
Пример типизации простого выражения
Проблема отложенной типизации в макросах
Решение
Трансформация сложных выражений с использованием информации о типах
Влияние на процесс типизации
VarType и FixedType
Реальное использование
Более простой способ наложить ограничение
Заключение
Ссылки

Макросы и типизация

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

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

Сразу хочу предупредить, что данный курс, и особенно данная (4-я) его часть, рассчитаны на кого угодно, но только не на новичков. Боюсь, что неподготовленный читатель, прочтя об описываемых мною трудностях, может испугаться и сказать что-то вроде: «О, нет... Макросы - это слишком сложно...». Средства должны быть очень простыми, если вы решаете простую задачу. Но если вы решаете сложную задачу, то и средства должны быть адекватно мощными. А мощные средства очень тяжело сделать простыми. Макросы – это мощное средство, позволяющее решать сложнейшие задачи, которые просто невозможно решить другими средствами. Но в то же время макросы могут быть очень простыми в использовании, если вы пытаетесь решить простую задачу, например, создать синтаксическое сокращение вроде описываемого в предыдущих частях оператора if/else. Если вы начали читать данную часть, и вам показалось, что все слишком сложно, просто бросьте ее чтение и начните с более простых вещей.

Почему получение информации о типах – это сложно?

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

Как работает система вывода типов?

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

Упрощенно процесс компиляции можно представить так. Компилятор:

  1. Считывает описание типов, импортируемых из внешних сборок.
  2. Производит пре-парсинг файлов, получая при этом дерево токенов, свернутых по скобкам. На этом этапе формируется так называемое «окружение». В окружении сохраняется информация об открытых пространствах имен, а значит, доступных макросах и операторах.
  3. Производит парсинг. Он состоит из разбора AST верхнего уровня (TopDeclaration) и разбора нетипизированного AST тел членов (PExpr).
  4. Строит дерево типов. Дерево строится на основании информации об импортированных из других сборок типов и на основании TopDeclaration разобранных исходных файлов.
  5. Производит разрешение наследования (задает отношения между типами).
  6. Типизирует члены типов, т.е. связывает текстовые описания типов параметров и возвращаемых значений с типами из дерева типов. Компилятор не допускает вывода типов для членов типов, поэтому после этого этапа становятся известными типы всех членов.
  7. Типизирует тела членов. Именно на этом этапе раскрываются макросы и производится вывод типов. Так что можно смело сказать, что это самая сложная стадия компиляции. На выходе этого этапа тела методов содержат типизированный AST (TExpr).
  8. Производят различные преобразования над типизированым AST, оптимизацию и т.п.
  9. Производит генерацию IL.

Под термином «типизации» подразумевается стадия типизации тел методов.

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

Упрощенно типизация тел методов состоит из:

  1. Чтения нетипизированного AST (PExpr) выражений в порядке их следования в тексте программы.
  2. Раскрытия макросов. При этом на выходе получается также нетипизированное AST, но не содержащее обращений к макросам.
  3. Типизации полученного на предыдущем шаге AST. На выходе этого шага получается типизированное AST (TExpr).
  4. Вычисление объектов отложенной типизации, которые могли получиться на предыдущей стадии.

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

Пока что все просто, но если еще чуть-чуть углубиться, то картина становится намного сложнее...

Типизация осуществляется объектом Typer (из пространства имен Nemerle.Compiler). Он читает каждое выражение (осмысленное сочетание веток AST) нетипизированного AST – PExpr. Проверяет, не совпадает ли выражение с шаблоном, описанным в одном из макросов уровня выражения (который «виден» в данной области видимости). Если выявляется совпадение, то выражение передается на обработку этому макросу. Перед передачей выражения макросу типизатор производит декомпозицию выражения в соответствии с шаблоном, описанным в макросе. При этом выражение может быть разбито на подвыражения для соответствия параметрам макроса.

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

        if (a > b) a - b else b - a

и в данной области видимости доступен макрос «if else», то компилятор выделит выражения:

a > b
a - b
b - a

и передаст их в соответствующие параметры стандартного макроса:

        macro @if (cond, e1, e2)
syntax ("if", "(", cond, ")", e1, Optional (";"), "else", e2) 
{
  <[ 
    match ($cond)
    { 
      | true => $e1
      | _ => $e2
    } 
  ]>
}

Цель макроса преобразовать переданные ему в качестве параметров выражения в некоторое выходное выражение, которое типизатор подвергнет дальнейшему процессу типизации. Если говорить более формально, макрос уровня выражения является функцией X -> PExpr, где X – это ноль или более PExpr, или массив PExpr (для эмуляции переменного числа аргументов). Если в возвращенном макросом коде встречаются другие макросы, то они также будут раскрыты.

Когда макросов больше не остается, типизатор типизирует полученное нетипизированное AST, преобразуя его в типизированное (TExpr).

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

Пример типизации простого выражения

        mutable x = 1;
WriteLine(x);
x = 2;

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

Итак, как же действует типизатор?

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

Встречая строку:

        mutable x = 1;

компилятор разделяет выражение на определение переменной:

        mutable x

и инициализирующее значение.

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

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

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

Изначально в компиляторе Nemerle для обозначения фиксированных типов использовалось имя MType (от Mono Type), а для обозначения переменных типов использовалось имя TyVar. Эти названия являются неинформативными и даже вводят людей в заблуждение, так как многие воспринимают слово «mono» как что-то, относящееся к платформе «Mono» компании Novell.

Далее в этой статье я буду использовать имя FixedType для обозначения фиксированного типа (в прошлом MType) и VarType (в прошлом TyVar).

В следующем разделе будет приведено описание типов VarType и FixedType. А пока что продолжим рассмотрение примера.

Итак, типизатор назначает «свежий» тип (т.е. новый экземпляр VarType) переменной «x» и переходит к типизации выражения. При этом от разбираемого выражения требуется, чтобы оно имело тот же тип, что и переменная. Это требование выражается в передаче типа переменной в качестве параметра «expected» функции типизации выражения. Так как выражение состоит из одного литерала, то типизатор довольно быстро выводит, что тип литерала, а стало быть, и всего выражения – int.

Далее производится проверка того, что тип выражения совпадает с типом переменной. Тип переменной не был задан явно, и, по сути, допускает любой тип. Это приводит к тому, что требование удовлетворяется, а переменная типа (переменной «x») начинает ссылаться на тип выражения, т.е. на int в нашем случае. Фактически переменная типа переменной «x» становится алиасом (alias) переменной типа выражения (точнее, переменные типа унифицируется, но об этом позже). Таким образом, типизатору становится ясно, что тип переменной – int.

Когда типизатор разбирает следующую строку, то обнаруживает, что в ней производится вызов статического метода WriteLine класса Console, которому передается выражение «x» (то есть ссылка или, иными словами, имя).

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

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

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

WriteLine(format : string, params arg : array[object]) : void
WriteLine(format : string, arg0 : object, arg1 : object, arg2 : object) : void
WriteLine(format : string, arg0 : object, arg1 : object) : void
WriteLine(format : string, arg0 : object) : void
WriteLine(value : string) : void
WriteLine(value : object) : void
WriteLine(value : ulong) : void
WriteLine(value : long) : void
WriteLine(value : uint) : void
WriteLine(value : int) : void
WriteLine(value : float) : void
WriteLine(value : double) : void
WriteLine(value : decimal) : void
WriteLine(buffer : array[char], index : int, count : int) : void
WriteLine(buffer : array[char]) : void
WriteLine(value : char) : void
WriteLine(value : bool) : void
WriteLine() : void

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

Так как известно, что тип переменной «x» – int, то компилятор может произвести разрешение перегрузки. В процессе разрешения перегрузки оказывается, что лучшей перегрузкой из подходящих является:

WriteLine(value : int) : void

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

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

        mutable x;
WriteLine(x);
x = 2;

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

Типизатор не может определить тип переменной «x» до того как он типизирует выражение «x = 2». Это приводит к тому, что разрешить перегрузку метода WriteLine на первом проходе не представляется возможным. Типизатор создает объект отложенной типизации – DelayedTyping, который инкапсулирует информацию о доступных перегрузках. Ссылка на этот объект помещается в типизированное AST. Таким образом, формируется не AST вызова конкретного метода, а заглушка, которая хранит информацию о том, что нужно произвести вызов некоторого, пока не определенного, метода из имеющегося списка.

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

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

В нашем примере фрагмент «x = 2» влияет на тип переменной «x», что приводит к тому, что объект отложенной типизации, содержащий список перегрузок метода WriteLine, может быть успешно типизирован. Таким образом, отложенная типизация позволяет использовать вывод типов не только из инициализации (как это происходит в C# 3.0), но и из использования. Более того. Из использования могут выводиться не только непосредственные типы, но и параметры типов. В следующем примере для типа Dictionary выводятся два параметра типов (TKey и TValue):

        def dic = Dictionary(); // указывать параметры типов явно нет нужды!
dic["test"] = 123;

В этом примере типизатор выводит, что первый параметр типа (TKey) для типа Dictionary должен быть равен string, а второй (TValue) – int.

Проблема отложенной типизации в макросах

Пример из предыдущего раздела я привел, чтобы объяснить вам следующую проблему.

Как я говорил выше, макросы раскрываются в процессе типизации. По сути, макрос оперирует с еще не типизированным AST. Однако ее наличие может резко повышает возможности макросов. В сущности, макрос, оперирующий только нетипизированным AST, весьма ограничен. Он может всего лишь произвести простейшую трансформацию кода (как, например, продемонстрированный макрос «if else»). Но для таких сложных макросов, как реализация собственных DSL (например, реализация поддержки linq), этого явно недостаточно.

Получить информацию о типах, вычисленную к моменту раскрытия макроса, очень просто. Это можно сделать двумя путями:

  1. Путем явной типизации PExpr с помощью метода Typer.TypeExpr(). Ему передается нетипизированное выражение (PExpr) и (необязательно) ожидаемый тип выражения.
  2. Обратившись к свойству PExpr.TypedObject. Оно заполняется компилятором в процессе типизации. Это свойство имеет тип object, так как в него могут помещаться объекты разного типа (например, типизированное представление параметра или типизированное выражение), но в подавляющем большинстве случаев в него помещается ссылка на соответствующий TExpr (т.е. на соответствующее типизированное AST). Учтите, что значение свойства TypedObject доступно только после того, как PExpr (напрямую или в составе другого PExpr) будет типизирован методом Typer.TypeExpr().

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

Если расширить наш пример следующим образом:

        mutable x;
WriteLine(MyMacro(x));
x = 2;

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

Решение

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

Но вот дилемма... Макрос должен что-то возвратить! Что же делать?

Для обхода подобных ситуаций в PExpr можно поместить заглушку. Вот ее описание:

        public
        variant PExpr : Located 
{
  ...
  | Typed { body : Typedtree.TExpr; }

Эта заглушка хранит ветку типизированного AST (TExpr), а в TExpr можно поместить тот самый объект отложенной типизации:

        public
        variant TExpr : Located
{
  ...
  | Delayed { susp : Typer.DelayedTyping; }

Данный объект также должен быть помещен и в очередь отложенной типизации, что приведет к тому, что типизатор будет вызывать его на стадии разрешения отложенной типизации, что в конечном итоге приведет к тому, что тип нужного нам выражения будет доступен, и мы сможем выполнить необходимые нам действия. Сделать это проще всего, воспользовавшись методом Typer.DelayMacro(). Ниже приведена общая схема (паттерн) применения этого подхода:

        macro SomeMacro(param1, param2, ..., paramN)
{
  def typer : Typer = Macros.ImplicitCTX();
  // какие-то действия по анализу и преобразованию параметров
  ...
  // Выполняет анализ передаваемого ему в качестве параметра // типизированного выражения и генерирует на основании этого анализа // выходное нетипизированное выражение.def makeResult(arg : TExpr) : PExpr
  {
    ...
  }

  def tExpr = typer.TypeExpr(param2);

  if (tExpr.Type.Hint.IsSome) // если тип известен...
    makeResult(tExpr) // ... выполняем пробразование немедленно!else
  { // иначе откладываем преобразование до лучших времен.
    typer.DelayMacro(lastTry =>
      if (tExpr.Type.Hint.IsSome) // если тип известен...
        Some(makeResult(tExpr)) // ... выполняем преобразование.else
      {
        when (lastTry)
          Message.Error(
            $"Тип выражения ‘$param2’ не был выведен (не растет кокос).");
        None()
      })
  }
}

В принципе все то же самое можно написать чуть проще, устранив одну из проверок, так как метод DelayMacro() сразу пытается произвести попытку вычислить отложенную типизацию, производя вызов переданной ему лямбды. Однако приведенный вариант может оказаться более эффективным, если тип оказался доступен сразу, так как при этом не происходит создания лишних объектов (лямбды с замыканием, option[T].Some(), PExpr.Typed, TExpr.Delayed, DelayedTyping и, возможно, некоторого количества других промежуточных объектов). Если ваш макрос планируется использовать широко, то лучше не полениться и сделать лишнюю проверку. Если же он используется не более нескольких десятков раз, то можно воспользоваться упрощенной схемой:

        macro SomeMacro(param1, param2, ..., paramN)
{
  def typer : Typer = Macros.ImplicitCTX();
  // какие-то действия по анализу и преобразованию параметров
  ...
  // Выполняет анализ, передаваемого ему в качестве параметра // типизированного выражения и генерирует на основании этого анализа // выходное нетипизированное выражение.def makeResult(arg : TExpr) : PExpr
  {
    ...
  }

  def tExpr = typer.TypeExpr(param2);

  typer.DelayMacro(lastTry =>
    if (tExpr.Type.Hint.IsSome) // если тип известен...
      Some(makeResult(tExpr)) // ... выполняем преобразование.else
    {
      when (lastTry)
        Message.Error(
          $"Тип выражения ‘$param2’ не был выведен (не растет кокос).");
      None()
    })
}

В лямбде, передаваемой методу DelayMacro, можно производить более сложный анализ. Так можно не просто узнать, вывелся тип или нет, а разобраться, что за тип вывелся, и возможно, заглянуть в его «потроха». Отличным примером, простым и одновременно информативным, такого макроса является макрос «lock» из стандартной библиотеки макросов Nemerle (см. http://nemerle.org/svn/nemerle/trunk/macros/core.n). Вот его код, снабженный комментариями:

        macro @lock (lockOnExpr, body)
syntax ("lock", "(", lockOnExpr, ")", body)
{
  def typer = Macros.ImplicitCTX();
  // Типизируем выражение, на результате которого производится блокировка...def lockOnTExpr = typer.TypeExpr(lockOnExpr);

  // Создаем объект отложенной типизации, принимающий лямбду // в качестве параметра.
  typer.DelayMacro(lastTry => 
    // значение параметра lastTry – true, означает, что производится// последняя попытка выполнить отложенную типизацию и требуется// выдать диагностическое сообщение о возможных причинах неудачи.

// получаем выведенный на данный момент тип выражения и разбираем его:match (lockOnTExpr.Type.Hint)
    {
      // Тип вычислен. Это FixedType.Class. Проверяем не является ли // этот тип типом-значением...
      | 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.
        Some(result)
    }
  );
}

Думаю, что приведенный код вкупе с комментариями ясен без дополнительных пояснений. Единственное, что хочется пояснить – это квази-цитату «$(lockOnTExpr : typed)». Такой вид цитат позволяет поместить в PExpr значение типа TExpr, то есть приведенный код аналогичен коду:

$(PExpr.Typed(lockOnTExpr))

Главное же, на что стоит обратить внимание в этом примере – это на анализ типов. Паттерн (с защитой):

| Some(Class(typeInfo, _)) when typeInfo.IsValueType

Распознает, что тип выражения является обычным типом (т.е. FixedType.Class, а не массивом, функцией и т.п.) с любым количеством любых параметров типа (задается знаком подстановки «_» во втором параметре конструктора). Этот паттерн связывает информацию о типе с переменной «typeInfo». Защита же (when) проверяет, является ли тип типом-значением.

СОВЕТ

Как работать непосредственно с типами, описанными в форматах FixedType и VarType, будет описано в одном из следующих разделов.

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

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

...
typer.DelayMacro(lastTry => 
  match (somePExpr.TypedObject)
  {
    | TExpr.Delayed(susp) when (susp.IsResolved) => // susp : Typer.DelayedTyping
      Some(doSomething(susp.ResolutionResult));

    | TExpr.Delayed(susp) =>       None();
    ...

Трансформация сложных выражений с использованием информации о типах

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

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

        public TransformWhenAllTypesWouldBeInfered(
  transform  : PExpr * TExpr -> PExpr
  tExpr      : TExpr, 
  expr       : PExpr = null, 
) : PExpr

Как видите, он принимает функцию трансформации, ветку типизированного AST (tExpr) и ветку нетипизированного AST (expr).

Последний параметр не обязателен, но надо учитывать, что если он не задан, то в первый параметр функции трансформации будет передан null.

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

Реализация этого метода довольно проста, так что проще продемонстрировать ее прямо здесь (снабдив, естественно, комментариями):

        public TransformWhenAllTypesWouldBeInfered(
  transform  : PExpr * TExpr -> PExpr
  tExpr      : TExpr, 
  expr       : PExpr = null, 
)
  : PExpr
{
  // ExprWalker позволяет обойти AST (как типизированное (TExpr), так и// нетипизированное (PExpr)) и произвести некоторые действия с его ветвями.def walker = ExprWalker();

  // Метод IsWellTyped() позволяет проверить содержит ли выражения// недотипизированные подвыражения (т.е. объекты отложенной типизации,// ветви, тип которых еще не выведен, или ошибки типизации).match (walker.IsWellTyped(tExpr))
  {
    // В дереве имеются ветви отложенной типизации или не выведенные типы,// но нет ошибок типизации.
    | WellTyped.NotYet => 
      this.DelayMacro(lastTime => 
          // Еще раз проверяем наличие недотипизированных подветкок.// Если все типизировано, то...if (walker.IsWellTyped(tExpr) == WellTyped.Yes)
            // производим трансформацию и возвращаем ее результат.
            Some(transform(expr, tExpr)) 
          elseif (lastTime)
// Нам не надо сообщать об ошибке, так как это сделает вложенный            // объект отложенной типизации или сам компилятор.
            None() 
          else
            None()
        );

    // Все подветки типизированы успешно.
    | WellTyped.Yes   => transform(expr, tExpr)
    // В подветках обнаружена одни или более ошибка типизации.
    | WellTyped.Error => PExpr.Error(tExpr.Location)
  }
}

Как видите, все просто и очевидно. Основная сложность ложится на объект ExprWalker.

Пример использования Typer.TransformWhenAllTypesWouldBeInfered() можно увидеть в коде реализации макроса «ToExpression» (поддержка linq в Nemerle):

http://nemerle.org/svn/nemerle/trunk/Linq/Macro/ToExpressionImpl.n

Там выражение, которое должно быть преобразовано в дерево выражений (т.е. в System.Linq.Expressions.Expression.Expression), сначала типизируется без преобразования (т.е. так, как будто оно не подвергается трансформации), а потом вызывается код преобразования в дерево выражения. Причем код преобразования вызывается c помощью метода TransformWhenAllTypesWouldBeInfered(). Это приводит к тому, что функция, занимающаяся непосредственно преобразованием, всегда работает с полностью типизированным AST, в котором не может встретиться ошибок типизации, не выведенных типов или не вычисленных объектов отложенной типизации. Это резко упрощает функцию преобразования.

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

Влияние на процесс типизации

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

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

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

Возможно, вы будете удивлены, но в подсистеме вывода типов Nemerle реализован небольшой интерпретатор языка Prolog. Точнее, не Prolog, а что-то похожее на него – Constraint solver. Constraint programming – это целая, относительно новая, парадигма декларативного программирования, о которой более подробно можно прочесть в Wikipedia:

http://en.wikipedia.org/wiki/Constraint_solver

http://en.wikipedia.org/wiki/Constraint_logic_programming

Для вывода типов типизатор Nemerle использует специальный объект Solver (constraint solver):

http://nemerle.org/svn/nemerle/trunk/ncc/typing/Solver.n

Вот перевод описания данного в этом файле:

Есть два вида переменных типов:

ПРИМЕЧАНИЕ

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

Решатель ограничений (constraint solver) поддерживает граф переменных типов (вершин) и отношений подтипов/наследования (ребер). Граф следует нескольким инвариантам:

  1. В нем есть только свободные переменные типов.
  2. В нем нет циклов. В случае появления цикла, все переменные типов, вовлеченные в него, сливаются в одну переменную типа. Таким образом, граф является DAG-ом (направленным ациклическим графом).
  3. Граф транзитивно замкнут, то есть, если A :> B и B :> C, то A :> C, где X :> Y означает ребро графа от X к Y.
  4. Нижняя и верхняя граница также транзитивно замкнуты, то есть если t :> A, A :> B, B :> t', то t :> t', где :> означает отношения наследования (subtyping relation).
  5. Если t :> A и A :> t', то t :> t' (то есть верхняя граница должна быть больше нижней). Если t = t', то тип t становится подстановкой для переменной А, поскольку есть только один тип, соответствующий и верхней, и нижней границе. При этом А становится фиксированной. Чтобы выполнялось правило 1, переменная удаляется из графа.

Иногда важно сохранить текущее состояние графа и затем вернуться к нему. Это делается с помощью методов PushState и PopState – они поддерживают стек отображений идентификаторов переменных типов на сами переменные типов.

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

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

Версионность накладывает одно ограничение. Если вы вызвали (явно или неявно) PushState(), то вы не сможете воспользоваться результатами типизации (т.е. выведенными типами и объектами отложенной типизации, а значит, и получившимся типизированным AST) после вызова PopState (). В случае спекулятивной типизации это вынуждает после ее успешного окончания повторять процесс типизации (чтобы получить результат).

VarType и FixedType

Чтобы понять, как Solver позволяет выводить типы, нужно описать типы FixedType и VarType. Начнем с описания класса VarType:

        public
        class VarType : System.IComparable [VarType]
{
  [System.Flags]
  publicenum Flags 
  {
    | None       = 0x0000
    | IsFromNull = 0x0001
    | IsMonoType = 0x0002
    | IsAliased  = 0x0004
    | IsFresh    = 0x0008
  }

  /// Определяет, что с этого времени @type, и this будет представлять   /// тот же самый тип. 
public Unify(@type : VarType ) : bool;
  /// То же самое, что и Unify, но в случае неудачи не портит значения
  /// переменной типа и не влияет на состояние компилятора.
public TryUnify(t : VarType ) : bool;

  /// Требовать, чтобы this был по крайней мере типом [t] (или его 
  /// наследником). Вызывайте этот метод, если требуется некоторое ограничение
  /// нижней границы типа.
  /// Возвращает true, если это возможно.
  /// Если это невозможно, то значение переменной типа будет испорчено и 
  /// может появиться сообщение об ошибке компилятора. Так что если нужно
  /// только проверить возможность данной операции, то нужно 
  /// воспользоваться методом TryRequire
publicvirtual Require(@type : VarType ) : bool;
  /// То же самое, что и Require, но в случае неудачи не портит значения
  /// переменной типа и не влияет на состояние компилятора.
public TryRequire (@type : VarType ) : bool;

  /// Задать тип @type как максимальный тип для this.
publicvirtual Provide(t : VarType ) : bool;
  /// То же самое, что и Provide, но в случае неудачи не портит значения
  /// переменной типа и не влияет на состояние компилятора.
public TryProvide(@type : VarType ) : bool;

  /// Нижняя граница типа.  
public LowerBound : option [FixedType];
  /// Верхняя граница типа.  
public UpperBound : option [FixedType]
  /// Если определены нижняя и/или верхняя граница типа, свойство Hint  /// возвращает mono-тип (фиксированный тип), запакованный в вариант
/// option.Some(). Этот тип аналогичен тому, который будет возвращен при  /// фиксации переменной типа (вызове метода Fix()).
  /// Однако в дальнейшем тип может быть уточнен (в процессе вывода типов).
  /// Если с переменной типа еще не ассоциировано никаких ограничений,
  /// данное свойство вернет option.None().
public Hint : option [FixedType];
  /// То же, что Hint. Разница заключается лишь в том, что в случае, когда
  /// тип имеет верхнее ограничение (UpperBound), заданное в типе object,
  /// AnyHint вернет его, а Hint вернет None(). Это происходит вследствие
  /// того, что смысл верхнего ограничения, заданного в object, практически
  /// отсутствует, так как object является базовым типом для всех остальных.
public AnyHint : option [FixedType];
  /// Фиксирует значение переменной типа. После этого данная переменная 
  /// уже не может быть изменена подсистемой вывода типов.
  /// НЕ ВЫЗЫВАЙТЕ ЭТОТ МЕТОД БЕЗ ОСОБОЙ НЕОБХОДИМОСТИ!!!public Fixate() : void;
  /// Возвращает фиксированное значение. Если переменная типа не была до
  /// этого фиксирована, будет сгенерировано исключение.
  /// Если вам нужно фиксированное значение, возможно, лучше воспользоваться
  /// свойствами Hint или AnyHint.publicvirtual FixedValue : FixedType;
  /// Фиксирует значение переменной типа и возвращает фиксированный 
  /// тип (FixedType). Аналогично последовательному вызову Fixate() и FixedValue.
  /// НЕ ВЫЗЫВАЙТЕ ЭТОТ МЕТОД БЕЗ ОСОБОЙ НЕОБХОДИМОСТИ!!!
publicvirtual Fix () : FixedType;

  [Nemerle.OverrideObjectEquals]
  public Equals (t : VarType ) : bool;
  /// Не поддерживается (не реализован).publicvirtual IsAccessibleFrom (_ : TypeInfo) : bool;
  /// Переменная типа является фиксированной (не может быть 
  /// изменена в будущем).
public IsFixed : bool { get; }
  /// Переменная типа не является фиксированной (аналогично !IsFixed).public IsFree : bool { get; }
  /// Переменная типа является свобоюной (не связанной): для нее не /// задана ни верхняя, ни нижняя граница (ограничение).public IsFresh : bool { get; }
  /// Переменная типа создана для переменной или параметра, которому /// присвоен null. Константа null не несет информации о типе, но /// добавляет ограничение «тип должен поддерживать null». Это ограничение/// допускает унификацию только со ссылочными типами или Nullable-типами.public IsFromNull : bool { get; set; }
  /// Тип может поддерживать null. CanBeNull становится равным true, если /// тип ссылочный, Nullable-тип или переменная является свободной (IsFree).publicvirtual CanBeNull : bool;

  public CurrentSolver : Solver { get; }

  /// Вызывает глубокую фиксацию, т.е. вызывает Fix() для данной переменной/// типа и для всех ее аргументов типа. Старайтесь не применять этот метод.public DeepFix() : FixedType;

  publicoverride ToString() : string;
  public CompareTo(other : VarType ) : int;
  publicoverride GetHashCode() : int;
}
ПРЕДУПРЕЖДЕНИЕ

Так как Nemerle – это язык, разработанный специально для .NET, вы можете получить в макросах ссылки на System.Type. Более того, у VarType есть метод GetSystemType(), возвращающий ссылку на System.Type. Этот метод вызывается при генерации IL. Однако вам, как разработчикам макросов, использование этого типа противопоказано (даже если вы генерируете код, который должен манипулировать с типом System.Type). В рамках макросов вы должны использовать только VarType и/или FixedType. Иначе вы обязательно столкнетесь с серьезными проблемами. Во-первых, вызов GetSystemType() приводит к «фиксации» типа, а значит, предотвращению его вывода в дальнейшем (например, вы можете получить тип object там, где в дальнейшем мог бы вывестись конкретный тип). Во-вторых, System.Type недоступен для типов, описанных в коде проекта, что может привести к некорректному поведению макроса в некоторых условиях.

Главные свойства этого класса:

ПРИМЕЧАНИЕ

Под «меньше» и «больше» понимается отношение подтипов. Скажем, если взять тип System.Collections.Generic.List[T], реализующий интерфейс System.Collections.Generic.IList[T], List[T] будет больше, чем IList[T]. IList[T], в свою очередь, реализует интерфейс System.Collections.Generic.ICollection[T], который меньше, чем IList[T]. Естественно, что ICollection[T] также меньше, чем List[T]. Понятие «больше» и «меньше» применимы только к типам, связанным отношением наследования (или реализации интерфейсов). При этом из рассмотрения исключаются базовые типы «object» и «System.ValueType» Например, типы int и long не могут сравниваться. Возможность использования значений типа int там, где требуется long, обеспечивается за счет неявного приведения типов.

На практике пользоваться свойствами LowerBound и UpperBound практически не приходится, но может оказаться полезным взглянуть на них для понимания того, что за тип выведется в итоге, и как это происходит.

Строковое представление переменной типа (выводимое также в отладчике) – это фактически значение свойства Hint, обработанное специальным образом. Свежие переменные обозначаются визуально как вопросительный знак – «?». Если для переменной задана только верхняя граница, то в конце к имени типа дописывается знак минус – «-». Если задана только нижняя граница, то к имени типа дописывается «+». Если же заданы и верхняя граница, и нижняя, то его строковое представлением будет выглядеть как «(L TILL H)», где L – это тип нижней границы, а H – верхней.

Что же приводит к установке верхней границы, а что – нижней?

Код вида:

        mutable x = System.Collections.Generic.List.[T]();

задает верхнюю границу переменной типа, ассоциированной с переменной «x».

Код вида:

        module X
{
  Foo(_ : System.Collections.Generic.IEnumerable[T]) : void { }

  Main() : void
  {
    mutable x;
    Foo(x); // тип задается вот здесь
  }
}

задает нижнюю границу переменной типа, ассоциированной с переменной «x».

А код вида:

        mutable x = null : System.Collections.Generic.IEnumerable[T];

задает и верхнюю, и нижнюю границы.

Естественно, что задать границы можно и по очереди.

Для задания ограничений (т.е. для построения графа типов) как раз и используются основные методы класса VarType.

Основными методами данного класса являются:

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

Таким образом, унификация может влиять сразу на ряд связанных переменных. Более подробно о процессе унификации можно прочитать в любом введении к языку Prolog или в Wikipedia (англ. http://en.wikipedia.org/wiki/Unification#Unification_in_logic_programming_and_type_theory). Фактически, выполняя type1.Unify(type2), мы говорим Solver-у, что обе переменные типа должны быть равны, а он уже автоматически перестраивает все зависимости так, чтобы выполнялись все инварианты. Так что Solver – это весьма непростой «инструмент»...

Если унифицировать переменные t2 и t3, а затем t1 и t2, то t1 и t3 также будут унифицированы.

Унификация с FixedType приводит к тому, что и верхняя, и нижняя граница переменной типа становятся равными фиксированному типу, с которым осуществлена унификация. Это приводит к тому, что переменная по сути становится алиасом для FixedType, с которым она унифицирована.

В случае невозможности унификации метод возвращает true.

СОВЕТ

Унификация может пройти неудачно. При этом переменные типа будут испорчены (не смогут участвовать в дальнейшем выводе типов, и их значения будут бесполезны), а стало быть, будет испорчено все состояние Solver-а. Для предотвращения порчи состояния Solver-а нужно использовать функции с префиксом Try (например, TryUnify) или явно вызвать PushState и PopState Solver-а для сохранения и восстановления его состояния.

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

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

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

Компилятор осуществляет унификацию во многих случаях. Например, при типизации операции присвоения инициализирующего значения переменной.

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

В случае невозможности наложения ограничений метод возвращает false. При этом состояние графа также портится, так что в случае, если нужно только лишь узнать, возможно ли наложение данного ограничения, лучше воспользоваться методом TryRequire.

Provide – фактически эта функция, обратная функции Require. Так что вызов x.Provide(y) эквивалентен y.Require(x). Впрочем, возможно я просто не смог уловить тонкого различия между этими методами. Как бы то ни было, метод Provide позиционируется как средство задания ограничения на верхнюю границу текущей переменной типа.

Чтобы продемонстрировать влияние методов Unify, Require и Provide на LowerBound и UpperBound переменных типов, я создал простой пример.

Прежде чем продемонстрировать код, я хочу рассказать о методе Typer.JustTry[T]. Он позволяет выполнить некоторые действия по типизации, так сказать, «в песочнице». JustTry получает ссылку на функцию типа void -> T, сохраняет контекст типизации, выполняет переданную функцию и восстанавливает контекст типизации. Таким образом, внутри функции, переданной в JustTry, можно производить любые действия типизации и наблюдать ее результаты, а после выхода из нее все изменения автоматически откатываются. Эта функция нужна в данном примере, чтобы продемонстрировать действия сразу всех трех методов (Unify, Require и Provide), не создавая трех отдельных тестов.

Ниже привден код макроса который получает два выражения в качестве параметров, типизирует их, выводит описание получившихся типов, а потом поочередно накладывает ограничения Unify, Require, Provide и выводит информацию о том, что при этом происходит с переменными типов:

        using Nemerle.Compiler;
using Nemerle.Compiler.Parsetree;
using Nemerle.Imperative;

namespace TypingTestMacroLibrary
{
  publicmacro Macro1(arg1, arg2)
  {
    Impl.Macro1(Nemerle.Macros.ImplicitCTX(), arg1, arg2)
  }
  
  module Impl
  {
    public Macro1(typer : Typer, arg1 : PExpr, arg2 : PExpr) : PExpr
    {
      // Типизируем оба выраженияdef tExpr1 = typer.TypeExpr(arg1);
      def tExpr2 = typer.TypeExpr(arg2);

      // Функция printType печатает выражение и его текущий тип def printType(expr : PExpr, ty : TyVar) : void
      {
        // Выводит сообщения только в основной проход компилятора, чтобы // сообщения не дублировались, если код содержит ошибки.when (typer.IsMainPass)
          // Печать производится с помощью выдачи подсказки компилятора, так// что его можно увидить в окне «Output» Visual Studio // во время компиляции.
          Message.Hint($"  $expr is '$ty'");
      }
      // Функция printTypeAndBounds печатает значение верхней и нижней границ типаdef printTypeAndBounds(expr : PExpr, ty : TyVar) : void
      {
        when (typer.IsMainPass)
        {
          printType(expr, ty);
          
          Message.Hint($"    LowerBound: '$(ty.LowerBound)'");
          Message.Hint($"    UpperBound: '$(ty.UpperBound)'");
        }
      }

      when (typer.IsMainPass)
        Message.Hint("Исходное состояние:");
      
      // Выводит описание типов до наложения на них дополнительных ограничений
      printTypeAndBounds(arg1, tExpr1.Type);
      printTypeAndBounds(arg2, tExpr2.Type);
        
      // Данная функция выполняет наложение ограничения, передаваемого в // качестве параметра func, и выводит информацию о том, что стало // с переменными типов.def test(methodName, func)
      {
        // Метод Typer.JustTry позволяет выполнить спекулятивную типизацию.// Результаты этой типизации будут доступны только внутри лямбды,// передаваемой в JustTry. После окончания работы лямбды состояние// Solver-а востанавливается.
        _ = typer.JustTry(fun()
        { 
          _ = func(tExpr1.Type, tExpr2.Type);
          
          when (typer.IsMainPass)
          {
            Message.Hint("");
            Message.Hint($"После: $arg1.$methodName($arg2):");
          }
          
          // Выводим информацию о типах после наложения ограничения.
          printTypeAndBounds(arg1, tExpr1.Type);
          printTypeAndBounds(arg2, tExpr2.Type);
          
          // Фиксируем переменные типов, заставляя тем самым Solver // вычислить окончательный тип.
          tExpr1.Type.Fixate();
          tExpr2.Type.Fixate();

          when (typer.IsMainPass)
            Message.Hint("После Fixate():");
          
          // Выводим текстовое представление типов после фиксации.
          printType(arg1, tExpr1.Type);
          printType(arg2, tExpr2.Type);
          
          42 // мы должны что-то вернуть
        })
      }
      
      // Поочередно выполняем тесты, накладывающие разные типы // ограничений и выводим результат.
      test("Unify",    (x, y) => x.Unify(y));
      test("Require",  (x, y) => x.Require(y));
      test("Provide",  (x, y) => x.Provide(y));
      test("Постышка", _ => true);

       <[ () ]>// Макрос уровня выражения должен вернуть хоть что-то.
    }
  }
}

Если этот макрос применить следующим образом:

        using TypingTestMacroLibrary;

module Program
{
  Foo(_ : System.Collections.Generic.IEnumerable[int]) : void {  }

  Main() : void
  {
    mutable a = System.Collections.Generic.List.[int]();
    mutable b;    
    Foo(b);
    
    Macro1(a, b);
    ignore(a); ignore(b);
  }
}

то в окне Output компилятора будет выведено:

Исходное состояние:
  a is 'System.Collections.Generic.List[int]-'
    LowerBound: 'None'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
  b is 'System.Collections.Generic.IEnumerable[int]+'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'None'

После: a.Unify(b):
  a is '(System.Collections.Generic.IEnumerable[int] TILL 
         System.Collections.Generic.List[int])'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
  b is '(System.Collections.Generic.IEnumerable[int] TILL 
         System.Collections.Generic.List[int])'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
После Fixate():
  a is 'System.Collections.Generic.List[int]'
  b is 'System.Collections.Generic.List[int]'

После: a.Require(b):
  a is '(System.Collections.Generic.IEnumerable[int] TILL 
         System.Collections.Generic.List[int])'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
  b is '(System.Collections.Generic.IEnumerable[int] TILL 
         System.Collections.Generic.List[int])'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
После Fixate():
  a is 'System.Collections.Generic.List[int]'
  b is 'System.Collections.Generic.List[int]'

После: a.Provide(b):
  a is 'System.Collections.Generic.List[int]-'
    LowerBound: 'None'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
  b is 'System.Collections.Generic.IEnumerable[int]+'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'None'
После Fixate():
  a is 'System.Collections.Generic.List[int]'
  b is 'System.Collections.Generic.List[int]'

После: a.Постышка(b):
  a is 'System.Collections.Generic.List[int]-'
    LowerBound: 'None'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
  b is 'System.Collections.Generic.IEnumerable[int]+'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'None'
После Fixate():
  a is 'System.Collections.Generic.List[int]'
  b is 'System.Collections.Generic.IEnumerable[int]'

Если немного изменить тест (выделено красным):

        using TypingTestMacroLibrary;

module Program
{
  Foo(_ : System.Collections.Generic.IEnumerable[int]) : void {  }

  Main() : void
  {
    mutable a = System.Collections.Generic.List.[int]();
    mutable b = [1, 2];
    
    Foo(b);
    
    Macro1(a, b);
    ignore(a); ignore(b);
  }
}

то будет выведено:

Исходное состояние:
  a is 'System.Collections.Generic.List[int]-'
    LowerBound: 'None'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
  b is '(System.Collections.Generic.IEnumerable[int] TILL list[int])'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (list[int])'

После: a.Unify(b):
  a is 'System.Collections.Generic.IEnumerable[int]'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (System.Collections.Generic.IEnumerable[int])'
  b is 'System.Collections.Generic.IEnumerable[int]'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (System.Collections.Generic.IEnumerable[int])'
После Fixate():
  a is 'System.Collections.Generic.IEnumerable[int]'
  b is 'System.Collections.Generic.IEnumerable[int]'

После: a.Require(b):
  a is '(System.Collections.Generic.IEnumerable[int] TILL 
         System.Collections.Generic.List[int])'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
  b is 'System.Collections.Generic.IEnumerable[int]'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (System.Collections.Generic.IEnumerable[int])'
После Fixate():
  a is 'System.Collections.Generic.List[int]'
  b is 'System.Collections.Generic.IEnumerable[int]'

После: a.Provide(b):
  a is 'System.Collections.Generic.IEnumerable[int]-'
    LowerBound: 'None'
    UpperBound: 'Some (System.Collections.Generic.IEnumerable[int])'
  b is '(System.Collections.Generic.IEnumerable[int] TILL list[int])'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (list[int])'
После Fixate():
  a is 'System.Collections.Generic.IEnumerable[int]'
  b is 'list[int]'

После: a.Постышка(b):
  a is 'System.Collections.Generic.List[int]-'
    LowerBound: 'None'
    UpperBound: 'Some (System.Collections.Generic.List[int])'
  b is '(System.Collections.Generic.IEnumerable[int] TILL list[int])'
    LowerBound: 'Some (System.Collections.Generic.IEnumerable[int])'
    UpperBound: 'Some (list[int])'
После Fixate():
  a is 'System.Collections.Generic.List[int]'
  b is 'list[int]'

Думаю, что внимательное рассмотрение результатов работы данных тестов поможет лучше понять принципы работы методов Unify, Require и Provide.

Реальное использование

Реально пользоваться методами Unify, Require и Provide приходится не так часто. Значительно чаще приходится пользоваться свойством Hint. Однако понимание принципов их работы помогает справляться со сложными задачами.

В качестве примере применении Require можно привести макрос foreach:

http://nemerle.org/svn/nemerle/trunk/macros/core.n (в этом файле надо искать @foreach).

В нем метод Require и его брат-близнец TryRequire применяются для определения того, реализует ли объект (получающийся в результате выражения, передаваемого в макрос foreach) интерфейсы IEnumerabl[T] и IDisposable. Вот фрагменты кода, проверяющего, реализует ли выражение интерфейс IDisposable:

        def
        is_disposable = typer.JustTry (fun()
  {
    def expr = typer.TypeExpr (init_body);
    expr.Type.Require (<[ ttype: System.IDisposable ]>)
  });

А вот, фрагмент, проверяющий, реализует ли тип IEnumerabl[T]:

        // Если вывелся какой-то типа...
| Some(ty) =>
  // Следующие две строки кода требуются для инициализации текущего // контекста. Это не более чем досадная недоработка в компиляторе, и по// уму эти строки не должны быть нужны. Однако на сегодня без них // квази-цитирование типа «ttype» не будет работать.def Manager = typer.Manager;
  Macros.DefineCTX(typer);
  // Получаем переменную типа, соответствующую фиксированному 
// типу IEnumerable[T].def iEnumerableType = 
<[ ttype: System.Collections.Generic.IEnumerable[_] ]>;
  
  // Проверяем, может ли переменная типа «ty» быть совместима с IEnumerable[T]if (ty.TryRequire(iEnumerableType))
  {
    // ...если может, то «жестко» требуем этого.
    ty.ForceRequire(iEnumerableType);
    // ... и продолжаем работу макроса. При этом тип уже точно // реализует IEnumerable[T]
    make(iEnumerableType.Hint)
  }
  else error(t)

В этом коде нужно объяснить две вещи: метод ForceRequire и квази-цитату типа ttype. ForceRequire – это просто версия метода Require, которая в случае неудачи выдает сообщение о внутренней ошибке компилятора. Вместо нее можно использовать и Require, но данный метод позволяет в случае чего легче выявить ошибку. Квази-цитата типа «ttype» – это тип квази-цитирования, позволяющий получить в результате не AST (PExpr), а фиксированную переменную типа, описывающую тип, заданный в цитате.

Получить ссылку на тип по его описанию можно и помощью метода Typer.BindType:

        def ty = typer.BindType(<[ System.Collections.Generic.IEnumerable[_] ]>);

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

Более простой способ наложить ограничение

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

        macro SomeMacro(expr)
{
  <[ $expr : IEnumerable[_]  ]>
}

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

Примером такого подхода может служить макрос while:

        macro @while (cond, body)
  syntax ("while", "(", cond, ")", body) 
  {
    def loop = Nemerle.Macros.Symbol(Util.tmpname("while_"));

    <[ 
      ($("_N_break" : global) : 
      {
        def $(loop : name)() : void 
        {
          when ($cond)
          {
            ($("_N_continue" : global) :
            {
              $body
            }) : void;  // речь вот об этом месте!
            $(loop : name)()
          }
        } 
        $(loop : name)(); 
      }) : void]>
  }

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

Заключение

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

Ссылки


Эта статья опубликована в журнале RSDN Magazine #1-2009. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 360        Оценить