Опуская технические детали, следует сказать, что сейчас мы в целом недовольнытем, как спроектированы семантические таблицы. В свое оправдание отметим, чтовсе "навороты" в них — вещи вполне объективные, которые так или иначе должныприсутствовать в компиляторе. Наша неудовлетворенность имеет, скорее,эстетическую природу: таблицы не выглядят стройной системой, где каждыйкомпонент точно подогнан к тому месту, которое для него предназначалось.

С деревом программы ситуация была обратной. Будучи один раз спроектированными,принципы организации дерева далее практически не изменялись. Впротивоположность таблицам, структура которых создавалась, чтобынепосредственно отражать контекстные отношения языка Си++, деревооказалось практически полностью языко-независимым. Иными словами, используяосновной строительный элемент дерева — терминальный узел — можно конструироватьпроизвольные конфигурации, отображающие конструкции любых языковпрограммирования. Все узлы дерева имеют идентичную структуру, различаясь лишьзначениями своих (немногочисленных) атрибутов. Каждый узел имеет четыре ссылки(вверх, вниз, влево и вправо), с помощью которых легко формировать "плоские"конфигурации, соответствующие тем или иным конструкциям входного языка. Какправило, горизонтальные ссылки отражают верхний уровень структуры некоторойконструкции, а вертикальные используются для поддеревьев, соответствующихэлементам этой конструкции, или вложенным конструкциям.

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

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

Этот раздел хочется завершить несколько неожиданным выводом. Наличие вкомпиляторе двух базовых структур — семантических таблиц и дерева программы — сейчас расценивается нами как один из самых серьезных недостатков компилятора.Эти структуры реализованы на различных принципах, работа с ними организованапо-разному, однако они существуют вместе, пронизаны взаимными ссылками (можносказать, переплетены, как корни растущих рядом деревьев) и в некоторых случаяхпросто дублируют друг друга. Сходная информация о структуре программыприсутствует и в таблицах, и в дереве, что долго приводило к путанице, и сейчасвыглядит довольно нелепо. Например, в дереве имеются узлы, соответствующиеобъявлениям; это естественно, так как образы объявлений могут попадать врезультирующий код. Что же касается таблиц, то они как раз и составлены наоснове информации, извлеченной из объявлений. Поэтому в узлах-объявленияхсодержится ссылка на соответствующее слово в таблицах. Семантическое слово, всвою очередь, имеет обратную ссылку на узел "своего" объявления, которая в рядеслучаев оказалась необходимой. Инициализатор переменной из объявленияпредставляется поддеревом, на которое имеются ссылки как из узла-объявления,так и из семантического слова. И так далее… Все это работает и даже вполнеэффективно, но, конечно, с точки зрения программного дизайна весьма далеко отсовершенства.

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

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

Лебедь, рак и щука, или Гадкий утенок

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

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

Так что шеф, глядя на нашу троицу с неподдельной гордостью, вполне мог считать,что вместе мы горы свернем. Однако далеко не все было гладко…

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

Но самая неприятная категория проектных ошибок — это те, которые возникли из-занедостаточно тщательного анализа на начальных этапах проекта и, что хуже, из-затого, что по некоторым принципиальным вопросам имелись различные мнения.Принимать решение всегда сложно еще и потому, что чье-то мнение, как правило,приходится отвергать. Тяжело и тому, кто отвергает, и неприятно тому, чьемнение не учитывается. Зачастую бывает так, что трудно предпочесть какой-либоконкретный вариант из нескольких альтернатив просто потому, что все онидостаточно обоснованы и могут быть использованы; в таких случаях необходимочье-то волевое решение, которое все участники должны безоговорочно принять. Унас в свое время просто не хватило духу проговорить все до конца и определитьсяполностью по всем принципиальным вопросам. В результате некоторые существенныерешения принимались "по умолчанию" тем или иным участником проекта безсогласования с другими. Винить в этом, естественно, следует прежде всегостаршего участника — автора этой статьи (как самого опытного, а не самогоумного!).

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


Перейти на страницу:
Изменить размер шрифта: