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

Работа с квазицитатами в Nemerle

Автор: Чистяков Владислав Юрьевич
Опубликовано: 11.04.2013
Исправлено: 10.12.2016
Версия текста: 1.1
Введение
Ссылки на имена
Разрешение имен
Как устроены имена?
Проблемы, которые могут возникнуть при использовании квазицитат
Изменения в сплайсах

Введение

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

Приставка «квази» означает, что цитаты могут иметь точки заполнения (placeholders), в которые можно вставлять готовый AST. Такие точки в Nemerle называются сплайсами (splice).

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

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

Ссылки на имена

Пожалуй, самой сложной и в то же время интересной темой является тема формирования ссылок на имена внутри квазицитат.

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

      macro Foo(expr)
{
  <[ 
    mutable x = 1;
    $expr;
    System.Console.WriteLine(x);
  ]>
}

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

Foo(x++);

то вместо изменения значения x мы получим сообщение об ошибке – «unbound name 'x'» (переменная "х" не объявлена).

а если так:

      using System.Console;
mutable x = 41;
Foo(x++);
WriteLine(x);

то получим на консоли:

1
42

так как «x» в макросе и «x» в рукописном коде – это разные переменные.

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

      int x = 41;
int value = 1;
x++;
Console.WriteLine(value);
Console.WriteLine(x);

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

Перепишем макрос следующим образом:

      macro Foo()
{
  <[  $("x" : usesite) = 100500; ]>
}

а его использование следующим:

      mutable x = 41;
x++;
WriteLine(x);
Foo();
WriteLine(x);

Этот код успешно скомпилируется, а при запуске выведет на консоль:

42
100500

это связано с тем, что сплайс $("x" : usesite) приказал компилятору сослаться на имя из внешнего контекста. А внешним контекстом является рукописный код.

Теперь давайте усложним ситуацию. Создадим макрос DefX, объявляющий переменную «x»:

      macro DefX()
{
  <[ mutable x = 41; ]>
}

и попытаемся обратиться к ней из другого макроса:

      macro UseX()
{
  <[  x = 100500; ]>
}

При попытке использовать эти макросы:

DefX();
UseX();

мы получим все то же сообщение о том, что переменная «x» не объявлена.

Ничего не удастся, если объявить макрос UseX следующим образом:

      macro UseX()
{
  <[  $("x" : usesite) = 100500; ]>
}

Происходит это потому, что макрос DefX гигиеничен. Он помечает все объявленные внутри него квазицитат имена собственным «цветом».

ПРИМЕЧАНИЕ

Цвет – это специальное значение используемое для различения имен в разных контекстах.

Чтобы обойти данное ограничение, нам нужно или объявлять и использовать имена с использованием сплайса «usesite»:

      macro DefX()
{
  <[ mutable $("x" : usesite) = 41; ]>
}

macro UseX()
{
  <[  $("x" : usesite) = 100500; ]>
}

или использовать для ссылки на имя сплайс «dyn»:

      macro DefX()
{
  <[ mutable x = 41; ]>
}
 
macro UseX()
{
  <[  $("x" : dyn) = 100500; ]>
}

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

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

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

Использование сплайса dyn значительно более опасно, так как компилятор будет пытаться захватывать первое попавшееся имя, доступное из точки применения макроса. Так что если это возможно, нужно пользоваться сплайсом usesite.

Nemerle поддерживает и еще один вид сплайса – «global». Имена, объявленные с его помощью, видны во всей программе. Перепишем макрос DefX следующим образом:

      macro DefX()
{
  <[ mutable $("x" : global) = 41; ]>
}

а код, использующий макросы, таким:

DefX();
UseX();
WriteLine(x);

этот код успешно скомпилируется, а при запуске выведет на консоль:

100500

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

Как же определиться с тем, какой из типов сплайсов использовать?

Мой ответ прост – пытайтесь вообще обойтись без слайсов. Если вам нужно сослаться на имя из другого контекста, то попытайтесь использовать сплайс типа usesite. Если вам нужно объявлять глобально видимые имена, то воспользуйтесь global. А если вам нужно сослаться на имя, и никакой другой тип сплайса использовать не удается, то используйте dyn.

В общем, помните, что dyn – это полное нарушение правил гигиены. Использовать его можно только в самом крайнем случае.

Разрешение имен

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

      using System.Console;

macro Print(expr)
{
  <[ WriteLine($expr); ]>
}

или

      using System;

macro Print(expr)
{
  <[ Console.WriteLine($expr); ]>
}

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

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

Как устроены имена?

Каждый макрос уровня выражения в Nemerle образует свой уникальный контекст, с которым ассоциируется уникальный «цвет». «Цвет» – это образное выражение, за которым скрывается целочисленный идентификатор, получаемый простым приращением.

Пользовательский код имеет цвет с фиксированным идентификатором, имеющим значение «1».

Имена, формируемые в квазицитатах, получают цвет, закрепленный за конкретным контекстом. Таким образом цвета во всех макросах уровня выражения и в рукописном коде не пересекаются. Это и называется «гигиеной». Гигиена предотвращает труднообнаружимые ошибки, которые часто встречаются в текстуальных макросах C/C++ и в макросах Lisp (где эта проблема обходится, но не так элегантно как в Nemerle).

Имя в Nemerle представлено классом Name:

      public
      class Name : ParsedBase, System.IComparable[Name], System.IEquatable[Name]
{
  public idl      : string;
  public color    : int;
  public context  : GlobalEnv;
  public ParsedId : string;

  publicthis(other : Name)
  {
    base(other.Location);

    idl       = other.idl;
    color     = other.color;
    context   = other.context;
    ParsedId  = other.ParsedId;
  }

  publicthis(id : string)
  {
    this(id, LocationStack.Top())
  }

  publicthis(id : string, loc : Location)
  {
    this(id, loc, id)
  }

  publicthis(id : string, color : int, context : GlobalEnv)
  {
    this(id, LocationStack.Top(), color, context, id)
  }

  publicthis(id : string, loc : Location, color : int, context : GlobalEnv)
  {
    this(id, loc, color, context, id)
  }

  publicthis(id : string, loc : Location, parsedId : string)
  {
    this(id, loc, ManagerClass.Instance.MacroColors.Color, null, // no global context
      parsedId);
  }

  publicthis(id : string, loc : Location, color : int, context : GlobalEnv, parsedId : string)
  {
    base(loc);

    this.color    = color;
    this.context  = context;
    idl           = id;
    this.ParsedId = parsedId;
  }

  staticpublic NameInCurrentColor (id : string, context : GlobalEnv) : Name
  {
    Name(id, context.Manager.MacroColors.Color, context)
  }

  staticpublic NameInCurrentColor (id : string, loc : Location, context : GlobalEnv) : Name
  {
    Name(id, loc, context.Manager.MacroColors.Color, context)
  }

  staticpublic NameInCurrentColor (id : string, parsedId : string, loc : Location, context : GlobalEnv) : Name
  {
    Name(id, loc, context.Manager.MacroColors.Color, context, parsedId)
  }

  staticpublic Global (mgr : ManagerClass, id : string) : Name
  {
    Name(id, 1, mgr.CoreEnv)
  }

  public NewName (id : string) : Name
  {
    Name(id, color, context);
  }

  /** Returns plain identifier string of this name.    */public Id : string
  {
    [DebuggerNonUserCode] get { idl }
  }

  publicoverride ToString() : string
  {
    Id
  }

  publicoverride GetHashCode() : int
  {
    unchecked (idl.GetHashCode() * (color + 1))
  }

  [Nemerle.OverrideObjectEquals]
  public Equals (other : Name) : boolimplements System.IEquatable[Name].Equals
  {
    this.CompareTo(other) == 0
  }

  public CompareTo(other : Name) : int
  {
    if (color == other.color || color < 0 || other.color < 0)
      if (idl == other.idl) 0
      elsestring.CompareOrdinal(idl, other.idl)
    else
      color - other.color
  }

  public GetEnv (default : GlobalEnv) : GlobalEnv
  {
    if (context != null)
      context
    elsedefault
  }
}

Вот описание его полей:

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

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

Проблемы, которые могут возникнуть при использовании квазицитат

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

Повторное использование может происходить как вследствие ручной манипуляции объектом типа Name, например:

      def n : Name = <[ x ]>.name;
  <[
    mutable $(n : name) = 41;
    $(n : name)++; // используется второй раз раза
  ]>

Так и при повторном применении одой и той же цитаты:

      def incrementExpr = <[ x++; ]>;
  <[
    mutable x = 41;
    $incrementExpr;
    $incrementExpr; // используется второй раз раза
  ]>

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

Например, проблемы могут возникнуть, если вы попытаетесь сгенерировать текст для кода, генерируемого макросом (например, с помощью метода DefineWithSource). При этом отладчик, идя по сгенерированному коду, будет то и дело «прыгать» с места на место. Это связано с тем, что при генерации текста его местоположение записывается в свойство Location объекта Name, для которого генерируется текст. Но из-за того, что один и тот же объект используется в разных местах, значение этого свойства переопределяется генератором теста, и в итоге оно всегда будет указывать на последнюю ссылку на имя.

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

      def n : string = "x";
  <[
    mutable $(n : usesite) = 41;
    $(n : usesite)++; // создается новый Name
  ]>

Или при повторном применении одной и той же цитаты:

      def incrementExpr() { <[ x++; ]> }
  <[
    mutable x = 41;
    $(incrementExpr());
    $(incrementExpr()); // создается копия AST
  ]>

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

Изменения в сплайсах

За последнее время поддержка сплайсов в Nemerle была несколько доработана. Среди улучшений можно отметить следующие:

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

      def value = "str";
// раньше было обязательно указывать тип
<[ $(value : string) ]>
// теперь это делать не обязательно
<[ $value ]>
// однако в сопоставлении с образцом это делать по прежнему обязательноmatch (expr)
{
  | <[ $(value : string) ]> => // OK
  | <[ $value ]> => // Так нельзя! Тип цитаты будет PExpr.

2. Сплайс «..$» теперь позволяет использовать не только значение типа list[PExpr], но и IEnumerable[PExpr], а также PExpr.Sequence (т.е. последовательности выражений, разделенных «;»).

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

Ниже приведен пример ко второму и третьему пункту.

      public
      macro M()
{
  def args1 = [<[ 3 ]>, <[ 4 ]>];
  def args2 = [<[ 6 ]>, <[ 7 ]>];

  def q = array[<[ def a = x + 1; ]>, <[ def b = a + x; ]>];
  def x = <[ { def c = b * 2; def d = c + a + x; } ]>;

  <[ 
    def tuple = (1, 2, ..$args1, 5, ..$args2);
    DebugPrint(tuple);
    def x = 2;
    ..$q;
    DebugPrint(a);
    ..$x;
    DebugPrint(b);
    DebugPrint(c);
    DebugPrint(d);
    d
  ]>
}

Использование:

System.Console.WriteLine(M());

Консольный вывод:

tuple ==> (1, 2, 3, 4, 5, 6, 7)
a ==> 3
b ==> 5
c ==> 10
d ==> 15
15


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