Индустрия программирования

Компилятор Си++



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

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

К сожалению, появление очередной версии стандарта не сопровождается указателем изменений по
отношению к предыдущей версии, поэтому контроль соответствия компилятора последней версии
превращается в отдельную весьма трудоемкую задачу, которая в весьма незначительной степени
может быть автоматизирована (например, путем использования процессоров сравнения


текстов).

В качестве примера радикального изменения языка можно привести главу 14 проекта стандарта
(Шаблоны). Первые версии определяли сравнительно простые средства параметризации периода
компиляции (правда, описание содержало большое количество неясных мест), которые допускали
простую модель реализации, основанную на "отложенной компиляции". Однако, в 1995 году
указанная глава подверглась кардинальной ревизии (ее объем увеличился в несколько раз) с
существенным расширением возможностей и усложнением синтаксиса и семантики. В основном это
было обусловлено стремлением поддержать возможности, заложенные в Стандартной Библиотеке
Шаблонов (Standard Template Library) Александра Степанова , которая в качестве составной
части вошла в проект стандарта. "Новые шаблоны" не допускали отложенную компиляцию, явно
требуя полного синтаксического и семантического контроля непосредственно в месте описания
шаблонов. Это привело не только к перепроектированию соответствующих компонент
компилятора, но затронуло очень многие, формально не связанные с шаблонами, его модули.


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

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

  • Препроцессор Си++;
  • Компилятор переднего плана Си++ (front-end compiler);
  • Генератор объектного кода.

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

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

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

Компилятор переднего плана. Эта компонента является центральной частью всего компилятора
Си++. Она воспринимает на входе текст программы на Си++ (в терминах стандарта - translation


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

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

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

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


выше, привела к существенному (около 2- х раз) сокращению объема синтаксиса по сравнению с
аналогичным синтаксисом, используемым в свободно распространяемом компиляторе GNU
C++.

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

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

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

Генератор кода. В настоящее время в компиляторе используется фирменный генератор объектного
кода для платформ Sun и Sparc, полученный от компании-разработчика формата внутреннего
представления. Напомним, что генератор, как и другие компоненты компилятора, представляет


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

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

Текущее состояние компилятора. В настоящее время разработка перешла в завершающую стадию.
Полностью реализован, отлажен и протестирован препроцессор. Компилятор переднего плана
практически полностью реализован; спроектированы все алгоритмы обработки "новых"
шаблонов, идет их реализация.

При тестировании компилятора переднего плана использовались средства аттестации,
создаваемые в рамках всего проекта (см. разд. 4). К началу октября 1996 г. компилятор успешно
обрабатывал около 97% тестов (используемый тестовый набор включает более 6000 тестов без
учета тестов на шаблонные конструкции). Проводились попытки сравнить степень соответствия
стандарту разрабатываемого компилятора с компиляторами известных фирм-производителей
инструментальных средств. Так, компилятор Watcom C++ версии 10.0 показал результат около
93%.

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

Содержание раздела