[N2] Типизация до раскрытия макросов vs. после раскрытия
От: VladD2 Российская Империя www.nemerle.org
Дата: 06.06.11 18:53
Оценка: 13 (2)
Подумал тут над нашим спором с WolfHound по поводу того до или после раскрытия макросов следует производить типизацию и пришел к выводу, что можно совместить оба подхода.

Предпосылки...

Очевидно типизация "по верхам", т.е. до раскрытия макросов, позволяет решить ряд проблем и дает IDE некоторую дополнительную информацию. Это особо полезно когда речь идет о подъязыках имеющих явно определенную и четко выраженную собственную семантику. Пример такого языка — PegGrammar. Он определяет грамматику которая обладает собственной семантикой.

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

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

На мой взгляд эти подходы можно спокойно объединить получив сочетания преимущество. Причем синтаксис полученного решения практически не будет отличаться от того что я описывал здесь
Автор: VladD2
Дата: 02.06.11
.

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

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

Таким образом вид макросов будет несущественно отличаться от того что описано здесь
Автор: VladD2
Дата: 02.06.11
.

Макрос состоит из:
1) шапки;
2) необязательного описания синтаксиса;
3) необязательного описания требований к типам;
4) частичное задание требований к типам;
5) необязательного блока трансформации.

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

Примеры макросов:
macro If is Expression
  syntax: "if" "(" condition ")" trueExpr "else" falseExpr
  require: condition :> #bool else Error("Условие if должно быть неявно приводимо к bool.")
  require: trueExpr has_convertion falseExpr else Error("Типы подвыражений должны приводиться друг к другу.")
  require: this unify trueExpr
{
  <[
     match ($condition)
     {
       | true  => $trueExpr
       | false => $falseExpr
     }
  ]>
}

Так как в выражениях требований типов не может встречаться ничего кроме типов, то для ссылки на типы параметров достаточно их имен (xxx.Type избыточно).
this — это ссылка на данный макрос. В приведенном выражении this означает "тип выражения описываемого данным макросом".
has_convertion — задает требование наличия неявного преобразования между типами. Это может быть унификация (на основе подтипов), оператор неявного приведения типов и т.п.
"#" в #bool — это это литерал типа (ссылка на тип). Другие примеры описания типов: #Dictionary[_, #int]. # — нужен для того чтобы отличать литерала типа от переменной типа.
Если описанный выше макрос применить таким образом:
if (1) 2.0 else 3

то будет выдана сообщение об ошибке:
Условие if должно быть неявно приводимо к bool.

если так:
if (true) 2.0 else "a"

то будет выдана сообщение об ошибке:
Типы подвыражений должны приводиться друг к другу.

Причем, в первом случае сообщение будет указывать на "1" (условие if-а), так как в "не сработавшем" require была ссылка только на condition, а во-втором на об выражения if-а, так как в "не сработавшем" require были ссылки на trueExpr и falseExpr.

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

С другой стороны можно написать этот же макрос так:

macro If is Expression
  syntax: "if" "(" condition ")" trueExpr "else" falseExpr
{
  <[
     match ($condition)
     {
       | true  => $trueExpr
       | false => $falseExpr
     }
  ]>
}

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

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

Параметры для которых заданы ограничения должны проходить типизацию до начала переписывания. Таким образом можно задать требование типизировать параметр перед переписыванием. Это позволяет реализовать макросы вроде foreach в декларативной форме. Ниже приводится фрагмент макроса foreach использующего эту возможность. Этот же пример демонстрирует новую возможность — перегрузку макросов по типам параметров (чего не было в Н1).
  macro ForEach is Expression
    syntax: "foreach" "(" pattern "in" collection ")" body
      where: var        = PatternWithGuard,
             collection = Expression,
             body       = Expression;
    require: collection as x && !x.IsPrimitive // "x" новая переменная типа равная типу collection
    require: x has method GetEnumerator() : e // проверяем соответствие паттерну "перечислитель"
    require: this unify #void
  {
    if (e has property Current : elemType
     && e has method   MoveNext() : #bool 
    )
      <[ 
        def e = $collection.GetEnumerator();

        while (e.MoveNext())
        {
           def $var = e.Current;

           $body
        }
      ]>
    else
      Error(collection, "The collection must implement enumerable pattern (see 8.8.4 The foreach statement of C# spec.).")
  }

  // Перегрузка макры ForEach. 
  macro ForEachIEnumarableT overload ForEach
    require: collection :> IEnumarable[_]
    require: this unify #void
    priority: < ForEach // Срабатывает только если не срабатывает основной вариант
  {
    <[
        def e = ($collection : IEnumarable[_]).GetEnumerator();

        while (e.MoveNext())
        {
           def $var = e.Current;

           $body
        }
    ]>
  }
  ...
  // Где-то в другом месте... возможно в другой сборке...
  // Так как тип array[_] более конкретный нежели IEnumarable[_], то для массивов 
  // срабатывает именно эта перегрузка!
  macro ForEachArray overload ForEach
    require: collection unify array[_]
    require: this unify #void
    priority: < ForEach
  {
    <[
       def ary = $collection
       for (mutable i = 0; i < ary.Length; i++)
       {
         def $var = ary[i];
         $body
       }
    ]>
  }

Ast.PatternWithGuard и Expression — это имена правил ассоциируемых с параметрами. Они же задают тип возвращаемого SST/AST.

Выражение "тип has method сигнатура" означает проверку наличия в типе "тип" требуемой сигнатуры метода. При этом так же осуществляется паттерн-матчинг и имена типов описанные без # связываются с соответствующими типами. Таким образом выражение:
e has property Current : elemType

проверяет наличие в типе "e" свойства с сигнатурой "Current : elemType" и связывает "elemType" с типом этого свойства. В свою очередь тип "e" формируется конструкций:
x has method GetEnumerator() : e

а "x" через связывание "as" (т.е. x равен типу выражения collection).
Обратите внимание, что типы "e", "x" и "elemType" определены на воплощенном типе, а стало быть это будут конкретные типы, а не параметры дженерик-типов.

Это такой декларативный DSL работы с типами.

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

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