Framework для создания языков программирования
От: VladD2 Российская Империя www.nemerle.org
Дата: 21.05.11 14:57
Оценка: 25 (3)
Всем привет!

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

В процессе дизайна Nemerle 2 родилась идея создания Framework-а для разработки языков программирования.

Первое — почему языков, а не компиляторов? Да потому что получается не просто библиотека упрощающая создание компилятора, а нечто что облегчает и обобщает разработку компилятора, интеграции с IDE и самого языка!

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

Состав фрэймворка

1. Сменные бэкэнды для чтение/записи метаданных и генерации исполняемых модулей.
2. Средства создания динамически (во время работы компилятора или IDE) расширяемых парсеров.
3. Средства создания специализированного (конкретного) синтаксического дерева. Я назвал это Specific Syntax Tree — SST.
4. Унифицированный AST покрывающий возможности всех подерживаемых языков.
5. Унифицированный движок типизации.
6. Унифицированый TAST (типизированный AST).
7. Плагинная система для всех стадий компиляции/обработки кода.

Цели фрэймворка

1. Упрощение создания компиляторов, средств интеграции с IDE, средств рефакторинга и т.п.
2. Обеспечение работы созданных языков на разных платформах (пока что в планах .Net, Java VM и LLVM (то бишь нэйтив)).
3. Возможность повторного использования грамматик и других частей компилятора/интеграции с IDE для разных языков. По сути это возможность использовать общую базу.
4. Автоматическое создание интеграции для IDE поддерживаемых фрэймворком для любого языка созданного на базе этого фрэймворка.
5. Унификация всех работ по разработке компилятора, интеграции к IDE, рефакторингов и т.п.

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

Кроме того языки созданные на таком фрэймворке можно будет очень легко расширять. А поддержка IDE для них будет почти бесплатной. Для реализации специализированных рефакторингов придется писать специализированный код, но за счет выноса многих аспектов в библиотеки, за счет унификации и компонентизации подобных решений можно будет существенно упростить создание таких рефакторингов. Многие рефакторинги вроде переименования идентификаторов будут реализованы один раз, и доступны для всех языков реализованных на базе фрэймворка.

Детали


Сменные бэкэнды

Идея сменяемых бэкэндов не нова, но почему-то до этого она все время оканчивалась некими непоследовательными действиями. Так есть бэкэнды для C/C++ (но на их базе можно создавать только бэкэнды для С/С++ и очень близких к ним языков), есть LLVM, есть виртуальные машины вроде дотнета и явы. Все это с натяжкой можно назвать бэкэндом.

Проблемы перечисленных выше бэкэндов заключается в следующм:
1. Они привязывают "архитектуре".
2. Они имеют весьма низкоуровневый интерфейс и разработчикам компиляторов приходится преобразовывать внутреннее представление компилятора в вызовы АПИ этого бэкэнда или вообще в некоторые структуры данных бэкэнда.

В предложенном фрэймворке предлагается сделать следующее. Бэкэнд на входе принимает TAST (типизированное TAST), а на выходе возвращает исполняемые модули со встроенными метаданными.

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

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

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

Средства создания динамически расширяемых парсеров

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

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

Вот как может выглядеть описание правила разбирающего класс языка аналогичного C#:
macro Class extend TypeDeclaration // макрос Class расширяет макрос (правило) TypeDeclaration
  syntax attributes modifiers partial "class" name typeParameters parents 
         constraints "{" members "}" ";"?
  where attributes     = CustomAttributes, // CustomAttributes - это макрос (правило)
        modifiers      = Modifiers, 
        partial        = PartialModifier, 
        name           = Identifier, 
        typeParameters = TypeParameters, 
        parents        = PExpr*, 
        members        = Member*;

Этот макрос аналогичен следующей грамматике:
class = attributes? modifiers? "partial"? "class" identifier type-parameter-list? 
        class-base? type-parameter-constraints-clauses? body ";"?

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

Квази-цитаты кода

Описав правило грамматки в виде макроса мы получаем не только парсер этого правила и возможность встроить его в некоторое место другого парсера (парсера языка). Так же мы получаем возможность описывать код в виде квази-цитат. Например, описание класса из предыдущего раздела позволяет описать в макросах класс используя следующую квази-цитату:
<[ PTypeDeclaration.Class:
   public class $name
   {
     $members
   }
]>


Кроме генерации кода квази-цитаты могут быть использованы и для декомпозиции кода (его АСТ). Таким образом мы можем распознать класс с именем Test используя паттерн-матчинг:
match (ast)
{
  | <[ PTypeDeclaration.Class:
   public class Test { $members } ]> => // делает что-то с членами класса

  | _ => // действия по умолчанию
}


Унифицированный AST

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

AST будет расширяться по худу добавления новых языков чтобы обеспечить необходимые им примитивы. Например, указатели для unsafe C#.

Specific Syntax Tree — SST

По сути это просто возможность создать свой AST специфичный для языка или DSL-я. Во фрэймворке будет возможность сформировать такой SST просто описав макрос (правило грамматики) без тела и указав имя вариантного типа который будет создан для этого.

SST позволит упростить обработку DSL-ей и частей языка сильно отличающихся от того что можно выразить в AST.

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

Унифицированный движок типизации

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

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

Типизатор таких языков как C#, VB.NET, Java, Scala, Delphi и Nemerle различаются очень несущественно. Их можно реализовать в едином классе, а разницу поведения обеспечить переопределением некоторых методов.

Типизация для языков типа F# или (тем более) С++ будет существенно отличаться, но и в них будет много общего. Думаю что плагины и полиморфизм вполне способны абстрагировать эти различия.

Унифицированый TAST

Типы будут доступны еще в обычном AST в процессе типизации. Но заниматься оптимизациями AST или генерировать исполняемый код по AST не очень удобно. Для облегчения этих задач AST, в результате работы типизатора, преобразуется в TAST — типизированное AST.

Именно AST передается бэкэндам для генерации исполняемого кода исполняемых модулей.

Плагинная система

Плагинная система не является отдельной подсистемой. Я выделил ее в отдельный пункт чтобы подчеркнуть, что на любой стадии компиляции или работы IDE можно будет встроить свой ("кастомный") код, который позволит сделать что-то нестандартное. Причем это "что-то" можно будет делать не только с кодом собственноручно написанного языка, а с кодом любого языка. Например, таким плагином может является оптимизатор кода или система генерации документации.

Поддержка IDE

Основные возможности возможности по поддержке IDE данный фрэймворк будет обеспечивать автоматически.

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

Переход к определению, поиск вхождений, рефакторинг переименования и прочие возможности будут осуществляться на основе TAST и AST.

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

Языки для которых можно создавать реализации на базе этого фрэймворка

Конечно не любой язык может быть воплощен с помощью такого фрэймворка. Но очень многие языки могут быть воплощены на его базе.

Конечно же в первую очередь речь идет о реализации статически типизированных языков поддерживающих классический (С++-ный) ООП, ФП и макросы в стиле немерла. Но хотя бы частично такой фрэймворк может быть использован для разработки почти любого языка.

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

Возможно, что можно создать и бэкэды для динамически-типизированных языков. Об этом я не думал, так как считаю динамику тупиковой веткой развития ЯП.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.