Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

СПО

.pdf
Скачиваний:
39
Добавлен:
31.05.2015
Размер:
2.9 Mб
Скачать

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

Рис. 26. Общая схема работы компилятора

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

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

языку и, более того, выявить правила, по которым эта цепочка была построена (поскольку на вопрос о принадлежности сам ответ «да» или «нет» представляет мало интереса). Интересно, что генератором цепочек входного языка выступает пользователь - автор исходной программы.

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

Рассмотрим перечень основных фаз (частей) компиляции и краткое описание их функций.

Лексический анализ (сканер) - это часть компилятора, которая читает литеры программы на исходном языке и строит из них слова (лексемы) исходного языка. Работа его заключается в следующем:

на вход лексического анализатора поступает текст исходной программы;

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

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

Синтаксический разбор - это основная часть компилятора на этапе анализа. Она предназначена для следующего:

выполняется выделение синтаксических конструкций

втексте исходной программы, обработанном лексическим анализатором;

проверяется синтаксическая правильность программы. Синтаксический разбор играет главную роль - роль рас-

познавателя текста входного языка программирования. Семантический анализ - это часть компилятора, прове-

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

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

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

сидентификацией элементов языка, распределением памяти и т. п.

Генерация кода - это фаза, непосредственно связанная с порождением команд, составляющих предложения выходного языка и в целом текст результирующей программы. Это основная фаза на этапе синтеза результирующей программы.

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

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

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

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

нилище данных должно быть организовано именно в виде таблиц или других массивов информации.

Представленное на рис. 26 деление процесса компиляции на фазы служит скорее методическим целям и на практике может не соблюдаться столь строго.

Далее рассмотрим общие аспекты взаимосвязи фаз компиляции:

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

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

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

8.1.5.Понятие прохода. Многопроходные и однопроходные

компиляторы

Для рассмотрения технических вопросов реализации основных фаз компиляции, необходимо познакомиться с понятием прохода.

Реальные компиляторы, как правило, выполняют трансляцию текста исходной программы за несколько проходов.

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

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

В качестве внешней памяти могут выступать любые но-

сители информации:

оперативная память компьютера;

накопители на магнитных дисках;

накопители на магнитных лентах и т. п.

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

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

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

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

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

Однопроходный компилятор, получающий на вход ис-

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

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

Реальные компиляторы выполняют, как правило, от двух до пяти проходов. Таким образом, реальные компиляторы яв-

ляются многопроходными.

Наиболее распространены двух- и трехпроходные компиляторы, например, выполняющие следующие проходы:

1)лексический анализ;

2)синтаксический разбор и семантический анализ;

3)генерацию и оптимизацию кода.

В современных системах программирования нередко первый проход компилятора (лексический анализ кода) выполняется параллельно с редактированием кода исходной программы.

8.2. Таблицы идентификаторов. Организация таблиц идентификаторов

8.2.1. Назначение таблиц идентификаторов

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

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

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

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

каторами.

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

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

Рассмотрим, что происходит с идентификаторами на различных фазах трансляции (табл. 16).

Таблица 16 Действия с идентификатором на различных фазах трансляции

 

Фаза компиляции

Действие с идентификатором

1.

Лексический анализ

выделение

2.

Синтаксический разбор

 

3.

Семантический анализ

определение характеристик

4.

Подготовка к генерации кода

 

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

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

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

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

Например, в таблицах идентификаторов может храниться следующая информация:

для переменных:

имя переменной;

тип данных переменной;

область памяти, связанная с переменной; для констант:

название константы (если оно имеется);

значение константы;

тип данных константы (если требуется); для функций:

имя функции;

количество и типы формальных аргументов функции;

тип возвращаемого результата;

адрес кода функции.

Приведенный выше состав хранимой информации, является только примерным. Конкретное наполнение таблиц идентификаторов зависит от реализации компилятора.

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

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

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

8.2.2. Принципы организации таблиц идентификаторов

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

рация поиска - чтобы убедиться, что такого элемента в таблице нет.

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

Можно выделить следующие способы организации таблиц идентификаторов:

простые и упорядоченные списки;

бинарное дерево;

хэш-адресация с рехэшированием;

хэш-адресация по методу цепочек;

комбинация хэш-адресации со списком или бинарным деревом.

8.2.3. Простейшие методы построения таблиц идентификаторов

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

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