
- •Содержание
- •Введение
- •1.3. Множества цепочек
- •1.4. Языки
- •1.5. Алгоритмы
- •1.6. Некоторые понятия теории графов
- •Контрольные вопросы
- •2. Введение в компиляцию
- •2.1. Задание языков программирования
- •2.2. Синтаксис и семантика
- •2.3. Процесс компиляции
- •2.4. Лексический анализ
- •2.5. Работа с таблицами
- •Переменная с плавающей точкой
- •2.6. Синтаксический анализ
- •2.7. Генератор кода
- •2.8. Оптимизация кода
- •2.8. Оптимизация кода
- •2.9. Исправление ошибок
- •2.10. Резюме
- •Контрольные вопросы
- •3. Теория языков
- •3.1. Способы определения языков
- •3.2. Грамматики
- •3.4. Распознаватели
- •3.5. Регулярные множества, их распознавание и порождение
- •5.2. LR(1) - таблица разбора
- •5.3. Построение LR – таблицы разбора
- •5.4. Сравнение LL – и LR – методов разбора
- •Контрольные вопросы
- •6. Оптимизация кода
- •6.1. Оптимизация линейного участка
- •6.1.1. Модель линейного участка
- •6.1.2. Преобразование блока
- •6.1.3. Графическое представление блоков
- •6.1.4. Критерий эквивалентности блоков
- •6.1.5. Оптимизация блоков
- •6.1.6. Алгебраические преобразования
- •6.2. Арифметические выражения
- •6.2.1. Модель машины
- •6.2.2. Разметка дерева
- •6.2.3. Программы с командами STORE
- •6.2.4. Влияние некоторых алгебраических законов
- •6.3. Программы с циклами
- •6.3.1. Модель программы
- •6.3.2. Анализ потока управления
- •Алгоритм вычисления прямого доминирования
- •6.3.3. Примеры преобразования программ
- •Удаления бесполезных операторов
- •Замена сложных операций
- •6.3.4. Оптимизация циклов
- •Перемещение кода
- •Индуктивное перемещение
- •Замена сложных операций
- •6.4. Анализ потоков данных
- •6.4.1. Интервалы
- •6.4.2. Анализ потоков данных с помощью интервалов
- •6.4.3. Несводимые графы управления
- •7. Включение действий в синтаксис
- •7.1. Получение четверок
- •7.2. Работа с таблицей символов
- •Контрольные вопросы
- •8. Проектирование компиляторов
- •8.1. Число проходов
- •8.2. Таблицы символов
- •8.3. Таблица видов
- •Контрольные вопросы
- •9. Распределение памяти
- •9.1. Стек времени прогона
- •9.2. Методы вызова параметров
- •9.3. Обстановка выполнения процедур
- •9.4. «Куча»
- •9.5. Счетчик ссылок
- •9.6. Сборка мусора
- •Контрольные вопросы
- •10. Генерация кода
- •10.1. Генерация промежуточного кода.
- •10.2. Структура данных для генерации кода
- •10.3. Генерация кода для типичных конструкций
- •10.3.1. Присвоение
- •10.3.2. Условные зависимости
- •10.3.3. Описание идентификаторов
- •10.3.4. Циклы
- •10.3.5. Вход и выход из блока
- •10.3.6. Прикладные реализации
- •10.4. Проблемы, связанные с типами
- •10.5. Время компиляции и время прогона
- •Контрольные вопросы
- •11. Исправление и диагностика ошибок
- •11.1. Типы ошибок
- •11.2. Лексические ошибки
- •11.3. Ошибки в употреблении скобок
- •11.4. Синтаксические ошибки
- •11.5. Методы исправления синтаксических ошибок
- •11.6. Предупреждения
- •11.7. Сообщения о синтаксических ошибках
- •11.8. Контекстно-зависимые ошибки
- •11.9. Ошибки, связанные с употреблением типов
- •11.10. Ошибки, допускаемые во время прогона
- •Контрольные вопросы
- •Список литературы

191
может быть представлен так, как показано на рис. 7.4.
struct |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int |
i |
|
|
struct |
|
|
|
real |
r |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Рис |
int |
J |
|
bool |
y |
|
||
|
|
|
7.4. Структура типа struct |
|
|
|
Каждая ячейка имеет два (возможно пустых) указателя; вертикальный указатель используется в случае структурных, объединенных и процедурных видов.
С помощью рассмотренного метода представления видов компилятор может легко выполнять следующие операции:
1)нахождение вида результата процедуры;
2)выбор структуры поля;
3)разыменование значения, т.е. замену адреса значением в адресе;
4)векторизацию, т.е. построение линейной структуры для любого массива.
Контрольные вопросы
1.Понятие прохода компилятора, необходимость использования нескольких проходов при компиляции.
2.Назначение таблицы символов.
3.Методы организации таблицы символов.
4.Хеширование, разрешение конфликтов при хешировании.
5.Сцепление элементов и бинарное дерево.
6.Назначение таблицы видов.
7.Способы организации таблицы видов.
9.Распределение памяти
9.1. Стек времени прогона
После выяснения структуры программы необходимо выделить место в памяти для внесения значений переменных и поместить соответствующие адреса в таблицу символов. Фаза распределения памяти практически не зависит от языка и машины и одинакова для большинства языков, имеющих блочную структуру. Распределение памяти за-
192
ключается в отображении значений, появляющихся в программе, на запоминающее устройство машины. Если реализуемый язык имеет блочную структуру, а ЭВМ имеет линейную память, то наиболее подходящим устройством, на котором будет базироваться распределение памяти, является стек или память магазинного типа.
Каждой программе для хранения значений переменных и промежуточный значений выражений необходим определенный объем памяти. Например, если идентификатор описывается как
int x,
т.е. х может принимать значение типа целого, то компилятору необходимо выделить память для х. Иными словами, компилятор должен вы-
делить достаточно места, чтобы записать любое целое |
число. |
|||||
Аналогично, если у описывается как |
|
|
|
|||
|
struct (int number, real size, bool n) y, |
|
|
|||
то компилятор обеспечивает для значенияу память с объемом, доста- |
||||||
точным для хранения в нем целого, вещественного и логического зна- |
||||||
чения. В |
обоих |
случаях |
компилятор |
не |
должен |
испытывать |
затруднений в вычислении требуемого объема памяти. Если z описывается
int z[10],
то объем памяти, необходимый для хранения всех элементов z, в 10 раз больше памяти для записи одного целого значения. Однако, если бы w был описан в виде
int w[n],
а значение n оказалось бы неизвестным во время компиляции(оно должно быть рассчитано программой), то компилятор не знал бы, какой объем памяти ему нужно выделить для w. Обычно w называют динамическим массивом. Память для w выделяется во время прогона. Память, выделяемую во время компиляции, называют статической, а во время прогона – динамической. В большинстве компиляторов память для массивов (даже имеющих ограничения констант) выделяется во время прогона, поэтому она считается динамической.
Память нужна также для промежуточных результатов и констант. Например, при вычислении выражения a + c ´ d сначала вычисляется c ´ d, причем значение запоминается в машине, а затем выполняется сложение. Память, используемая для хранения результатов, называется рабочей. Рабочая память может быть статической и динамической.
В каждом компиляторе предусмотрена схема распределения памяти, которая до некоторой степени зависит от компилируемого языка. В Фортране память, выделяемая для значений идентификаторов, никогда не освобождается, так что подходящей структурой является одномер-

193
ный массив. Если считать, что массив имеет левую и правую стороны, память может выделяться слева направо. При этом применяется указатель, показывающий первый свободный элемент массива. Например, в результате описания
INTEGER A, B, X, Y
выделяется память, как это показано на рис. 8.1.
A B X Y
Рис. 8.1. Распределение памяти для Фортрана
Такая схема не учитывает тот факт, что рабочая память используется неоднократно и весьма неэффективна для языка с блочной структурой.
В языке, имеющем блочную структуру, память обычно высвобождается при выходе из блока, которому она выделена. В этом случае оптимальным решением было бы разрешить указателю отодвигаться влево при высвобождении памяти. Такой механизм распределения эквивалентен стеку времени прогона или памяти магазинного типа.
Пусть имеется программа вида: begin real x, y
.
.
.
begin int c, d
.
.
.
end
begin char p, q
.
.
.
end
.
.
.
end
На рис. 8.2 показаны «моментальные снимки» стека времени прогона на различных этапах ее выполнения.

|
|
194 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
d |
|
|
q |
||
|
|
|
c |
|
|
p |
||
y |
|
y |
|
|
y |
|||
x |
|
x |
|
|
x |
|||
(1) |
|
|
(2) |
|
|
|
(3) |
|
|
|
Рис. 8.2. Стек времени прогона |
|
|
На рис. 8.2 изображено место, занимаемое значениями идентификаторов во время прогона. Часть стека, соответствующую определенному блоку, называют рамкой стека. Указатель стека показывает на его первый свободный элемент.
Кроме указателя стека требуется также указатель на дно текущей рамки (указатель рамки). При входе в блок этот указатель устанавливается равным текущему значению указателя стека. При выходе из блока сначала указатель стека устанавливается равным значению, соответствующему включающему блоку. Указатель рамки включающего блока может храниться в нижней части текущей рамки стека, образуя часть статической цепи или массива, который называется дисплеем. Его можно использовать для хранения во время прогона указателей на начало рамок стека, соответствующих всем текущим блокам (рис. 8.3).
q
p
x
ДИСПЛЕЙ y СТЕК
Рис. 8.3. Система «дисплей-стек»
Это упрощает настройку указателя при выходе из блока.
Если бы вся память была статической, адреса времени прогона могли бы распределяться во время компиляции, и значения элементов дисплея также были бы известны во время компиляции.
Рассмотрим пример программы: begin int n; read(n);
int numbers[n]; real p;
begin real x, y;
Место для numbers должно выделяться в первой рамке стека, а для х и у – в рамке над ней. Но во время компиляции неизвестно, где должна начинаться вторая рамка, так как неизвестен размер чисел. Одно из решений в этой ситуации – иметь два стека: один для статиче-

195
ской памяти, распределяемой в процессе компиляции, а другой для динамической памяти, распределяемой в процессе прогона.
Другое решение заключается в том, чтобы при компиляции выделять статическую память в каждом блоке в начале каждой рамки, а при прогоне – динамическую память над статической в каждой рамке. Это значит, что когда происходит компиляция, неизвестно, где начинаются рамки, но можно распределить статические адресаотносительно начала определенной рамки. При прогоне точный размер рамок, соответствующих включающим блокам, известен, так что при входе в блок нужный элемент дисплея всегда можно установить так, чтобы он указывал на начало новой рамки (рис. 8.4).
|
|
Рамка |
|
у |
|
2 |
|
х |
|
|
|
|
Дина- |
|
|
Числа |
миче- |
|
|
ская |
|
||
|
|
||
|
часть |
Рамка |
|
|
|
1 |
|
p |
Ста- |
||
|
nтическая
часть
Дисплей Стек
Рис. 8.4. Стек прогона для массива В этой структуре массив занимает только динамическую память. Од-
нако некоторая информация о массиве известна во время компиляции, например его размерность (а, следовательно, и число границ – две на каждую размерность), и при выборке определенного элемента массива она может потребоваться. В некоторых языках сами границы могут быть неизвестны при компиляции, но всегда известно их число, и для значений этих границ можно выделить статическую память. Тогда можно считать, что массив состоит из статической и динамической частей. Статическая часть массива может размещаться в статической части рамки, а динамическая – в динамической. Кроме информации о границах, в статической части может храниться указатель на сами элементы массива (рис. 8.5).

|
|
196 |
|
|
|
|
|
|
|
|
Рамка |
|
|
|
|
|
|
|
|
у |
|
|
2 |
|
|
х |
|
|
|
|
|
Эле- |
|
Дина- |
|
|
|
мен- |
|
|
|
|
|
|
миче- |
|
|
|
|
ты |
|
|
|
|
|
|
ская |
Рамка |
|
|
|
чисел |
|
||
|
|
|
часть |
1 |
|
|
|
|
|
||
|
|
|
|
Ста- |
|
|
|
p |
|
|
|
|
|
|
|
тиче |
|
|
|
Стати- |
|
ская |
|
|
|
ческая |
|
часть |
|
|
|
часть |
|
|
|
|
|
чисел |
|
|
|
|
|
n |
|
|
|
|
|
|
|
|
|
Дисплей |
|
Стек |
|
|
|
Рис. 8.5. Модифицированный стек прогона для массива
Когда в программе выбирается конкретный элемент массива, его адрес внутри динамической памяти должен вычисляться в процессе прогона. Пусть имеется массив
int table[1:10, -5:5].
Будем считать, что элементы массива записаны в лексикографическом порядке индексов, т.е. элементы таблицы хранятся в следующем порядке:
table[1, -5], table[1, -4]………. table[1, 5], table[2, -5], table[2, -4]………. table[1, 5],
.
.
.
table[10, -5], table[10, -4]………. table[10, 5].
Адрес конкретного элемента вычисляется как смещение по отношению к базовому адресу (адресу первого элемента) массива:
ADDR(table[I , J ]) = ADDR(table[l1, l2 ])+ (u2 - l2 +1)´ (I - l1 )+ (J - l2 ).
Здесь l1 и u1 - нижняя и верхняя границы первой размерности и т.д. и считается, что элемент массива занимает единицу объема памяти.
Выражение (ui - li +1) задает число различных значений, которые может принимать i-й индекс. Расстояние между элементами, отли-

197
чающимися на единицу в i-м индексе, называется i-й шагом и обозначается si . Пример шагов массива (рис. 8.6):
|
int N[1:5, 1:5, 1:5] |
||||||
N[1,1,1] |
N[1,1,2]..N[1,1,5] N[1,2,1]… N[2,1,1] …N[5,5,5] |
||||||
|
s1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
s2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
s3 |
|
|
|
|
|
|
|
|||||
|
Рис. 8.6. Схема смещений |
|
Если бы каждый элемент массива занимал объем памятиr, то эти шаги получили бы умножением всех вышеприведенных величин на r.
Ясно, что вычисление адресов элементов массива в процессе прогона может занимать много времени. Но шаги могут вычисляться только один раз и храниться в статической части массива наряду с границами. Такая статическая информация называетсяописателем массива.
Во многих языках все идентификаторы должны описываться в блоке, прежде чем можно будет вычислять какие-либо выражения. Отсюда следует, что рабочую память нужно выделять в конкретной рамке стека, над памятью, предусмотренной для значений, соответствующих идентификаторам (называемой стеком идентификаторов). Обычно статическая рабочая память выделяется в вершине статического стека идентификаторов, динамическая рабочая память – в вершине динамического стека идентификаторов.
В процессе компиляции статический стек идентификаторов растет по мере объявления идентификаторов. Вместе с тем, статический рабочий стек может не только увеличиваться в размерах, но и уменьшаться. Пример:
x=a+b´ c.
При вычислении выражения (a+b´ c) потребуется рабочая память, чтобы записать b´ c перед сложением. Ту же самую рабочую память можно использовать для хранения результатов сложения. Однако после осуществления операции присвоения этот объем памяти можно освободить, так как он уже не нужен.
Динамическая память должна распределяться во время прогона, статическая же распределяется во время компиляции. Объем статической рабочей памяти, который должен выделяться каждой рамке, оп-