Типизатора Н2 – версия VladD2
От: VladD2 Российская Империя www.nemerle.org
Дата: 01.06.11 21:07
Оценка:
Пожалуй что мое представление о том каким должен быть типизатор Н2 созрело и я могу им поделиться.

Некоторые предпосылки... из чего я исхожу...
1. Следует изобретать новые идеи только тогда когда старые не удовлетворяют требованиям и нет путей изменить это.
2. Создание макросов в Н2 должно упроститься и не в коем случае не усложняться. Ради упрощения создания сложных макросов нельзя жертвовать простотой написания простых.
3. Если есть выбор между декларативным описанием и кодом, то лучше предпочесть декларацию (К.О.).
4. Framework разрабатываемый в процессе работы над Н2 должен брать на себя максимум работы. Писатель макросов и создатель новых языков должен делать только необходимый минимум.
5. Поддержка IDE и бэкэндов должны даваться получаться автоматически, за исключением тех фич котоыре которые полностью зависят от языка (например, рефакторинги). Для зависящих от языка фич должны предоставляться стандартные решения упрощающих реализацию.

Для того чтобы было понятнее как проходит процесс типизации придется начать с парсинга. Итак синтаксический макрос Н2 будет описывать:
1. Правило грамматики разбирающее строку.
2. Ветку SST описывающую все данные разобранные из строки. По сути это будут отдельные вхождения вариантного типа + разная информация необходимая для работы IDE (о токенах, подсветке, фолдинге, ...).
3. Тело макроса, вызываемое для раскрытия макроса.
4. Необязательные декларации, описывающие требования к макросу и его параметрам.

Стадии работы:
1. Парсинг. На этой стадии файл проекта парсится макросами, которые загружаются для отдельных типов файлов и могут по ходу разбора кода подгружать дополнительные макросы в точки ресширения. В конце этого процесса мы получаем CompileUnit содержащий SST (Specific Syntax Tree) файла.
2. Производится раскрытие макросов уровня типов не требующих информации о типах. В результате получается набор TypeBuilder-ов (возможно их нужно назвать особо, но подходящее имя в голову не приходит) по одному для каждого типа. Это не окончательные TypeBuilder-ы, так как могут быть partial-типы. На этом этапе TypeBuilder-ы содержат только MemberBuilder-s – типы описывающие AST членов. Эти типы содержат SST тел и преобразованные в стандартное AST описание члена. Кроме того TypeBuilder и MemberBuilder могут содержать любую дополнительную информацию обеспечивающую их специализацию.
3. Производится слияние TypeBuilder-ов всего проекта и формирование дерева типов проекта. В результате получается дерево ветвями которого являются пространства имен, а листьями – TypeBuilder-ы и реализации ITypeInfo полученные из внешних сборок и других проектов.
4. Выполнятся макросы уровня типов не требующие информации о наследовании.
5. Производится предварительное вычисление наследования и реализации интерфейсов для TypeBuilder-ов.
6. Выполняются макросы которым требуется информация о наследовании, но не требуется информация о типах членов.
7. Производится связывание типов членом.
8. Производится разложение MemberBuilder-ов на MethodBuilder-ы. В конечном счете в исполнимом коде могут быть только функции. Значит все члены типов должны быть в итоге преобразованы в них. Например, свойства должны быть преобразованы в методы гетыры и/или сетеры.
8. Производится связывание типов сгенерированных методов.
9. Выполняются макросы которые зависят от типов членов.
10. Производится типизация тел методов. До этого момента в них находится SST, точнее корнем является AST-ветка-заглушка которая ссылается на SST. На выходе получается TAST. Кроме того в процессе работы генерируется AST. При этом в ветки SST помещается ссылки на сгенерированные по ним ветку AST и TAST.
11. Результат работы – TypeBuilder-ы, MethodBuilder-ы и TAST передается бэкэнду который по ним генерирует исполняемые модули в которые помещаются код и метаданные.

Описание процесса типизации тел методов привожу отдельно...

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

Обратите внимание на то, что под термином «типизация» вы можете понимать нечто отличное от того что понимаю я!

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

Процесс типизации тела метода:
1. Типизатор обрабатывает ветку AST метода одну за одной (начиная с корневой ветки ссылка на которую находится в MethodBuilder).
2. Если ветка является ссылкой на SST, то производится процесс раскрытия макроса.
3. Полученная в результате раскрытия ветка макроса анализируется и если это снова оказывается ссылка на SST, то выполнение продолжается с пункта 1.
4. Если текущая ветка не ссылка на SST, то вызывается функция трансформации AST в TAST. В процессе этой трансформации вычисляются типы AST, производится разрешение перегрузки и т.п. Кроме всего прочего ссылка на TAST помещается в AST, а ссылка на AST в SST. Таким образом, после окончания процесса типизации выражения, по SST можно получить порожденное по нему AST и TAST. Точно такие же ссылки помещаются и TAST, так что через любой вид деревьев можно получить связанные с ними другие виды деревьев.

В процессе типизации ветки AST получают ссылки на TAST и типы. Среди веток TAST могут быть неоднозначные ветки. Если в TAST имеются неоднозначные ветки, производится повторный проход по TAST и попытка разрешить неоднозначности. Если в процессе повторного прохода изменился список неоднозначностей, то производится еще одна попытка до тех пор, пока список неоднозначностей не станет пуст – что считается успешным завершением типизации, или пока список неоднозначностей не перестанет изменяться – что является ошибкой типизации. В процессе разрешения неоднозначностей одни ветки TAST могут заменяться на другие. Например, MemberAccess заменен на Overloaded, а Overloaded на Resolved.


Процесс раскрытия макроса:
1. SST-ветке может соответствовать более одного макроса (Н2 будет поддерживать перегрузку макросов по условиям), по этому процесс раскрытия макроса включает процесс разрешения перегрузки макроса.
2. Получается список макросов соответствующих раскрываемому SST.
3. Список сортируется по приоритетам и его элементы перебираются в порядке убывания приоритета.
4. Для каждого макроса проверяются декларативные условия. Если условие истинно, запускается макрос (раскрытие макроса). В ином случае производится проверка условия для следующего элемента списка.
5. Если макрос успешно раскрылся, перебор списка останавливается, а результат раскрытия возвращается процессу типизации. Если в процессе раскрытия макроса произошла ошибка, то результат раскрытия запоминается и процесс продолжается с пункта 4.
6. Если декларативные условия не были удовлетворены ни для одной перегрузки выдается сообщение об ошибке (которое может быть задана авторами макроса или сформирована на основе декларативных условий).
7. Если некоторые из макросов были запущены, но ни один из них не смог успешно раскрыться, то формируется ветка Ast.Errors() в которую помещается список сообщений об ошибках выданный обломавшимися макросами.

Примеры

Пример перегруженного макрос (foreach):
  macro ForEach : Ast.Expression
    syntax: "foreach" "(" pattern "in" collection ")" body
      where: pattern    = Ast.PatternWithGuard,
             collection = Ast.Expression,
             body       = Ast.Expression;
    require: collection.Type as X && !X.IsPrimitive
    require: X has <[ method GetEnumerator() : $E ]>
  {
    <[ /* реализация по умолчанию реализующая паттерн "перечислитель" */ ]>
  }

  // Перегрузка макры ForEach. 
  macro ForEachIEnumarableT overload ForEach
    require: collection.Type is IEnumarable[_]
    priority: < ForEach // Срабатывает только если не срабатывает основной вариант
  {
    <[ /* реализация для IEnumarable[T] */ ]>
  }
  ...
  // Где-то в другом месте... возможно в другой сборке...
  macro ForEachArray overload ForEach
    require: collection.Type is array[_]
    priority: > ForEachIEnumarableT // попытка раскрыть этот макрос будет предпринята перед ForEachIEnumarableT
  {
    <[ /* реализация для массива */ ]>
  }



Декларативные условия и порядок типизации

В макросах Н2 можно будет задать декларативные условия. Они позволяют описать зависимость макроса от типов выводимых для параметров макроса.

По умолчанию макрос получает на вход нетипизированное SST. Если макрос хочет чтобы ему на вход пришло типизированное SST для некоторого параметра, то он может задать условие (путем добавления секции «require»).

Минимальным условием является требование наличия выведенного типа. Это может выглядеть так:
require: collection.Type

Кроме того можно задать более конкретное условие, что тип должен быть фиксированным:
require: collection.Type is fixed

это подразумевает, что должен быть выведен как сам тип, так и всего его параметры типов (если таковые имеются).
Конструкция:
require: collection.Type is fixed[]

означает, что ожидается что выведен хотя бы сам тип, без учета параметров типов. «fixed» — это ключевое слово заменяющее любой имя типа.
Конструкция:
require: collection.Type is fixed[_]

означает, что должен быть выведен тип имеющий один параметр типа. Через запятую можно указывать нужное количество параметров типов.
Конструкция:
require: collection.Type is Name[_]

означает, что ожидается тип Name с одним параметром типов (вместо Name может быть подставлено любое имя типа, например IEnumarable). Аналогично fixed могут быть вариации с другим количеством параметров типов или без них.
Конструкция macroParameter.Type as Name позволяет задать имя для переменной типа. Далее в выражениях можно оперировать этим именем.
Так же в require могут использоваться операторы «&&» и «||», а так же произвольные выражения над типами (использующими API типов). Например, !X.IsPrimitive (из примера выше) означает, что тип не должен быть примитивным (например, int).
Так же возможно задавать условия на наличие у типа некоторых членов. Например:
X has <[ method GetEnumerator() : $E ]>

означает, что у типа X ожидается наличие метода GetEnumerator. Причем тип E можно в дальнешем использовать в рамках макроса или других условий. Например, мы можем проверить, что у типа E есть свойство Current:
E has <[ property Current : $D ]>

Если тип задан без $, то можно ссылаться на конкретные типы. При этом параметры типов так же могут быть переменными. Кроме того можно использовать «_» или ключевое слово fixed.

Условие
priority: < ИмяДругогоМакроса

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

Если в макросе задано условие на тип одного из параметров макроса, то компилятор вначале раскрывает и типизирует SST находящееся в этом параметре, а потом вызывает макрос передавая ему SST в котором имеется ссылка на AST и TAST, а поле Type содержит ссылку на выведенный тип.

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

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