Генерация кода

(источник: http://www.wl.unn.ru/~ragozin/compiler/compil/g11.htm)

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

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

Представление в виде ориентированного графа

Простейшей формой промежуточного представления является синтаксическое дерево программы. Более полную информацию о входной программе дает ориентированный ациклический граф (ОАГ), в котором в одну вершину объединены вершины синтаксического дерева, представляющие одни и те же константы, переменные и общие подвыражения. На рис. 1 приведены оба ПП программы.

Рис. 1. Промежуточные представления программы.

Трехадресный код

Трехадресный код- это последовательность операторов вида x:= y op z, где x,y и z - имена, константы или сгенерированные компилятором временные объекты. Здесь op - двуместная операция, например операция плавающей или фиксированной арифметики, логическая или побитовая. В правую часть может входить только один знак операции.

Составные выражения должны быть разбиты на подвыражения, при этом могут появиться временные имена (переменные). Смысл термина "трехадресный код" в том, что каждый оператор обычно имеет три адреса: два для операндов и один для результата. Трехадресный код - это линеаризованное представление синтаксического дерева или ОАГ, в котором явные имена соответствуют внутренним вершинам дерева или графа. Например, выражение x+y*z может быть протранслировано в последовательность операторов

t1:=y*z
t2:=x+t1

где t1 и t2 - имена, сгенерированные компилятором.

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

if A>B then S1 else S2

может быть представлен следующим кодом:

t:=A-B
JGT t,S2
.....

Здесь JGT - двуместная операция условного перехода, не вырабатывающая результата.

Разбиение арифметических выражений и операторов управления делает трехадресный код удобным (???) при генерации машинного кода и оптимизации. Использование имен промежуточных значений, вычисляемых в программе, позволяет легко переупорядочивать трехадресный код.
t1 := -c
t2 := b * t1
t3 := -c
t4 := b * t3
t5 := t2 + t1
a := t5
t1 := -c
t2 := b * t1
t5 := t2 + t2
a := t5
а) б)
Рис. 2.

Представления синтаксического дерева и графа рис. 1 в виде трехадресного кода дано на рис. 2. а) и 2. б), соответственно.

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

Четверка - это запись с четырьмя полями, которые будем называть op, arg1, arg2 и result. Поле op содержит код операции. В операторах с унарными операциями типа x:=-y или x:=y arg2 не используется. В некоторых операциях (типа "передать параметр") могут не использоваться ни arg2, ни result. Условные и безусловные переходы помещают в result метку перехода. Обычно содержимое полей arg1, arg2 и result - это указатели на входы таблицы символов для имен, представляемых этими полями. Временные имена вносятся в таблицу символов по мере их генерации.

Чтобы избежать внесения новых имен в таблицу символов, на временное значение можно ссылаться, используя позицию вычисляющего его оператора. В этом случае трехадресные операторы могут быть представлены записями только с тремя полями: op, arg1 и arg2. Поля arg1 и arg2 - это либо указатели на таблицу символов (для имен, определенных программистом, или констант), либо указатели на тройки (для временных значений). Такой способ представления трехадресного кода называют тройками. Тройки соответствуют представлению синтаксического дерева или ОАГ с помощью массива вершин.

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

Линеаризованные представления

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

Таким образом, постфиксная запись (обратная польская) - это список вершин дерева, в котором каждая вершина следует непосредственно за своими потомками. Дерево на рис. 1 в постфиксной записи может быть представлено следующим образом:

a b c - * b c * + :=

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

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

:= a + * b - c * b - c

Организация информации в генераторе кода

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

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

Уровень промежуточного представления

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

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

Некоторые формы промежуточного представления лучше годятся для различного рода оптимизаций (например, ориентированные графы), некоторые - хуже (например, префиксная запись для этого плохо подходит). Алгоритм перевода выражения в ПОЛИЗ

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

Рассмотрим алгоритм.

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

Оператор Приоритет
- (унарный) 1
not 2
^^ 3
*, /,% 4
+, - 5
and, or, xor 6
(, ) 100

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

Пример разбора показан в таблице.

Входная строка Стек операций Выходная строка
a*b/c+a*b+(a-c)*d    
*b/c+a*b+(a-c)*d   a
b/c+a*b+(a-c)*d * a
/c+a*b+(a-c)*d * ab
c+a*b+(a-c)*d / ab*
+a*b+(a-c)*d / ab*c
a*b+(a-c)*d + ab*c/
*b+(a-c)*d + ab*c/a
b+(a-c)*d +* ab*c/a
+(a-c)*d +* ab*c/ab
(a-c)*d + ab*c/ab*+
a-c)*d +( ab*c/ab*+
-c)*d +( ab*c/ab*+a
c)*d +(- ab*c/ab*+a
)*d +(- ab*c/ab*+ac
*d + ab*c/ab*+ac-
d +* ab*c/ab*+ac-
  +* ab*c/ab*+ac-d
    ab*c/ab*+ac-d*+

Синтаксически управляемый перевод на примере перевода арифметического выражения в ПОЛИЗ.

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

Для этого сделаем несколько дополнений, в частности мы можем ввести операции с наивысшим приоритетом:

Оператор Приоритет
:= 101
^,. 0

Так как выражение присваивания обычно имеет вид <переменная>:= <выражение>, то приоритет операции присваивания должен быть менее приоритета скобок, а приоритет операций ссылки по указателю, и ссылки на структуру наивысший. Отдельно отметим, что операция адресации "@" прямо не является операцией, а лишь префиксом, который отменяет последнюю адресацию, хотя при соответствующем генераторе кода с возможностью отмены последней ссылки адресация может быть обработана как обычная операция в стеке.

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

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

Представление условных операторов, операторов цикла и оператора безусловного перехода в ПОЛИЗ.

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

JMP     метка

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

pop     ax
test    ax,ax
jnz     метка_перехода

Генерация кода на языке ассемблер, для вычисления арифметического выражения.

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

Рассмотрим таблицу случаев генерации кода для процессора 8086/8087, для случая - один для 8086, другой для сопроцессора 8087. Условимся, что вершина арифметического стека кешируется в 8086 процессоре в регистре bx.
Действие Код для 8086 Код для 8087
:= pop ax ; адрес
xchg ax,bx
mov [bx],ax ; двигаем
fstp dword ptr [bx]; в bx адрес
pop bx
Внесение левой части в стек (или любого адреса) push bx
mov bx, offset var
Внесение переменной в стек push bx
mov bx, var
fld var
Унарный минус neg bx fchs
Бинарная операция: вычитание pop ax
xchg ax,bx
sub bx,ax
fsub
Бинарная операция: сложение pop ax
add bx,ax
fadd
Ссылка по адресу mov bx,[bx]
Ссылка на структуру mov bx,[bx+offset field]
Тернарная условная операция test bx,bx
pop bx
pop ax
jz exit1
xchg ax,bx
exit1: ...
fistp temp
wait
mov ax,temp
and ax,ax
jz exit2
fxch
exit2: fincstp
ffree st
Загрузка значения по адресу mov bx, [bx] fld dword ptr [bx]

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

Генерация кода для операторов READ и WRITE языка ПАСКАЛЬ.

Напомним формат вывода и ввода для языка Паскаль, например:

write[ln]('File has ',l,' lines and ',w,'words');

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

Рассмотрим генерацию кода для приведённого оператора:

str_1 db 9,"File has ";
str_2 db 10,"lines and"
str_3 db 5,"words"
Определим в сегменте данных три строки в формате Паскаль
...  
push offset str_3
push STRING
Адрес строки и ярлык строки
push offset w
push INTEGER
Адрес целого числа, опции форматирования можно добавить к ярлыку
push offset str_2
push STRING
Адрес строки и ярлык
push offset l
push FLOAT
Адрес плавающего числа и ярлык - пусть число строк будет плавающим
push offset str_1
push STRING
Адрес строки и ярлык
push 5 Число параметров
call _writeln_ Собственно вызов функции
...  

Генерация кода для условного оператора IF, вложенного оператора IF языка ПАСКАЛЬ.

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

if (условие)
then
test bx,bx
jne метка_ХХХ
Генерируем код с уникальным именем метки, имя этой метки вместе с ярлыком _IF_ ложим на стек
... else ... jmp метка_YYY
метка_XXX:
Здесь мы достаём из стека ярлык _IF_ вместе с сгенерированным ранее именем метки, вставляем эту метку в ассемблеровский листинг, затем ложим на стек новую уникальную метку YYY с ярлыком _ELSE_
... ; метка_YYY:
Достаём из стека ярлык _ELSE_ с меткой, которую вставляем в листинг

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

Генерация кода для оператора цикла FOR, вложенного оператора FOR языка ПАСКАЛЬ.

Продолжаем обсуждение, но уже рассматривая цикл FOR:

FOR var:=Value1 push dx
mov dx, Value1
Инициализируем индексную переменную - регистр dx, сохраняя прежнюю в стеке
TO|DOWNTO Value2 loop_XXX: cmp dx,Value2
ja|jb loop_exit_XXX
Здесь мы генерируем уникальную метку (на самом деле только суффикс XXX), которую мы ложим на стек вместе с ярлыком _FOR_ и пометкой, увеличивается или уменьшается в цикле индексная переменная
DO ... ... ;тело цикла в dx - индексная переменная
... ; inc|dec dx
jmp loop_XXX
loop_exit_XXX:
pop dx
Вынимаем из стека управляющих операторов ярлык _FOR_, генерируем инкремент или декремент индексной переменной, генерируем переход к началу цикла, метку окончания цикла и восстанавливаем регистр, содержащий индексную переменную

Аналогично, вряд ли требуются дополнительные комментарии и ясно, как поддерживаются вложенные операторы FOR.

Генерация кода для операторов цикла WHILE, REPEAT языка ПАСКАЛЬ.

Закончим обсуждение генерации кода примером для условным циклов. Первым будет цикл WHILE.

WHILE (условие)
DO
w_loop_XXX:
...;код вычисления
...;условия
test bx,bx
jz w_loop_exit_XXX
Здесь мы генерируем уникальную метку (реально только уникальный суффикс), которую мы ложим на стек вместе с ярлыком _WHILE_, заметим, что между метками располагается не показанный здесь код вычисления условия, который возвращает булево значение на вершине стека
... ; jmp w_loop_XXX
w_loop_exit_XXX:
Вынимаем из стека управляющих операторов ярлык _WHILE_, генерируем переход к началу циклаb метку окончания цикла

Аналогичный пример для REPEAT:

REPEAT ...
r_loop_XXX:
...
Здесь мы генерируем уникальную метку, которую мы ложим на стек вместе с ярлыком _REPEAT_
UNTIL(условие); ...;код вычисления
...;условия
test bx,bx
jnz r_loop_XXX
Вынимаем из стека управляющих операторов ярлык _WHILE_, генерируем код вычисления условия, который возвращает булево значение на вершине стека и генерируем переход к началу цикла

Аналогично, вряд ли требуются дополнительные комментарии и ясно, как поддерживаются вложенные операторы WHILE/REPEAT.