Работа с представлением GENERIC в gcc - часть 3
Вступление
В предыдущей статье мы научились создавать простейшую функцию main
,
которая могла возвращать какое-то значение. Кроме того рассмотрели работу
основных элементов представления, и теперь можем переходить к непосредственно
созданию основных языковых конструкций.
Основные конструкции
Будем предполагать что из предыдущей статьи мы научились создавать лексические
блоки, цепочки определений и списки выражений, и к началу этой части у нас уже
есть простая функция main
, которая возвращает 0. Т.е. будем считать
что в файле x1.cc
у нас уже содержится код из первых трёх примеров
второй заметки. Весь дальнейший код будет добавляться между концом второго и
началом третьего примера.
Объявление переменных
Мы уже писали код объявления переменных, но рассмотрим его ещё раз:
Код в gcc | Аналог на Си | GENERIC |
---|---|---|
//--------------------- Пример 1
|
int var1;
|
--------------- | VAR_DECL | | var1 | | int | --------------- | --------------- | VAR_DECL | | var2 | | bool | --------------- | --------------- | VAR_DECL | | var3 | | float | --------------- |
Помимо добавления кода из примера, потребуется в уже существующем коде исправить конструкцию
// Теперь соединяем блок кода со списком выражений
tree bind = build3 (
BIND_EXPR, // Тип узла
void_type_node, // Тип выражения
NULL_TREE, // Список объявлений
stm_list, // Список выражений
body); // Охватывающий лексический блок
на
// Теперь соединяем блок кода со списком выражений
tree bind = build3 (
BIND_EXPR, // Тип узла
void_type_node, // Тип выражения
var_decl_1, // Список объявлений
stm_list, // Список выражений
body); // Охватывающий лексический блок
В этой конструкции мы поменяли третий аргумент с нулевого указателя на узел с первым объявлением. Теперь код можно спокойно собрать и посмотреть на результат.
Перейдём к описанию кода из примера. Для создания объявления переменной
используется узел с типом VAR_DECL
, который создаётся через вызов
функции build_decl
. Первым операндом для данного узла является
узел с символьным именем, а вторым - узел с типом переменной. В gcc присутствует
довольно много встроенных типов переменных, их можно найти в файле
tree.h
. Также стоит отметить что помимо функции
get_identifier
, создающей узел с заданным идентификатором,
существует функция create_tmp_var_name
, которая создаёт временное
имя, используя заданный префикс. Не лишним будет напомнить что переменную
var_decl_1
следует добавить в код создания связующего узла.
Объявление констант
Код в gcc | Аналог на Си | GENERIC |
---|---|---|
//--------------------- Пример 2
|
int var1 = 10;
|
--------------- | VAR_DECL | | var1 | | int | | 10 | --------------- | ..... | --------------- | VAR_DECL | | var3 | | float | | 3.4 | --------------- |
Здесь мы ограничились двумя константами - целочисленной и плавающей. С первой
всё довольно просто: создаётся при помощи функции build_int_cst
.
Первым аргументом функции является тип константы, до размеров которого она
автоматически расширяется, а вторым - непосредственно значение.
С созданием плавающей константы всё несколько сложнее. Для неё необходимо
завести специальную переменную типа REAL_VALUE_TYPE
, которая будет
хранить её значение. Задать значение простым литералом тоже нельзя, его можно
только сконвертировать из целого числа, или, как у нас, строки функцией
real_from_string
. Такие сложности связаны с тем что представление
плавающих чисел зависит от конкретной платформы, а в GENERIC необходимо работать
с ними независимо от целевой платформы.
Далее у нас идёт использование макроса DECL_INITIAL
, который для
переменных задаёт значение при инициализации.
Присваивание значения
Ещё один пример, с которым мы уже сталкивались ранее - присваивание значения:
Код в gcc | Аналог на Си | GENERIC |
---|---|---|
//--------------------- Пример 3
|
var2 = 1;
|
--------------- /| var2 | ---------------/ --------------- | MODIFY_EXPR | ---------------\ --------------- \| 1 | --------------- |
Для этого примера потребуется поднять наверх выражение:
// Список выражений
tree stm_list = alloc_stmt_list ();
В самом примере ничего нового не появилось, присваивание выполняет узел с типом
MODIFY_EXPR
, у выражения данного узла тип всегда
void_type_node
. К сожалению я не очень понял почему он создаётся
при помощи функции fold_build2
, но так делают все, и мы будем.
Первым операндом выражения является адресат присвоения значения, вторым - само
присваиваемое значение.
Арифметика
Более интересный пример с арифметическими выражениями:
Код в gcc | Аналог на Си | GENERIC |
---|---|---|
//--------------------- Пример 4
|
var1 = var1 * 3 - 10;
|
--------------- --------------- /| var1 | /| var1 | ---------------/ --------------- ---------------/ --------------- | MODIFY_EXPR | /| MULT_EXPR | ---------------\ ---------------/ ---------------\ --------------- \| MINUS_EXPR | \| 3 | ---------------\ --------------- --------------- \| 10 | --------------- |
Работа с арифметикой довольно проста: большинство выражений создаётся функцией
build2
, т.к. имеет левую и правую часть. В нашем случае мы
использовали узлы умножения MULT_EXPR
и минуса
MINUS_EXPR
. Самих по себе типов арифметических узлов несколько
больше, их можно разделить на следующие группы:
- Целочисленная и плавающая арифметика
PLUS_EXPR
MINUS_EXPR
MULT_EXPR
TRUNC_DIV_EXPR
CEIL_DIV_EXPR
FLOOR_DIV_EXPR
ROUND_DIV_EXPR
TRUNC_MOD_EXPR
CEIL_MOD_EXPR
FLOOR_MOD_EXPR
ROUND_MOD_EXPR
MIN_EXPR
MAX_EXPR
ABS_EXPR
- ...
- Битовые операции
LSHIFT_EXPR
RSHIFT_EXPR
LROTATE_EXPR
RROTATE_EXPR
BIT_IOR_EXPR
BIT_XOR_EXPR
BIT_AND_EXPR
BIT_ANDTC_EXPR
BIT_NOT_EXPR
...
- Логические операции
LT_EXPR
LE_EXPR
GT_EXPR
RROTATE_EXPR
GE_EXPR
EQ_EXPR
NE_EXPR
...
Ещё одной особенностью, которую можно заметить является то что арифметические выражения не были добавлены в список выражений. В списке будет лежать только выражение присваивания, а код для его операндов создастся автоматически.
Выражение возврата
С ним мы уже сталкивались не раз, теперь наконец разберём их:
Код в gcc | Аналог на Си | GENERIC |
---|---|---|
//--------------------- Пример 5
|
return var1;
|
--------------- /| RESULT_DECL | --------------- ---------------/ --------------- | MODIFY_EXPR |--| RETURN_EXPR | --------------- ---------------\ --------------- \| var1 | --------------- |
Само по себе выражение возврата представляется с узлом типа
RETURN_EXPR
. У этого узла есть только один операнд, который требует
выражение присваивания к переменной возврата (либо узел с константой). В случае
если функция значения не возвращает, то операндом будет NULL_TREE
.
Безусловный переход
Перед тем как изучить управляющие конструкции, нам необходимо рассмотреть возможность перехода на заданную метку, т.к. условия и циклы основаны именно на таких переходах.
Код в gcc | Аналог на Си | GENERIC |
---|---|---|
//--------------------- Пример 6
|
goto label_1;
|
--------------- /| GOTO_EXPR | ---------------/ --------------- | LABEL_DECL | ---------------\ --------------- \| LABEL_EXPR | --------------- |
Конструкция goto состоит из трёх основных элементов. Во-первых это узел типа
LABEL_DECL
с объявлением метки. Это объявление добавляется в
цепочку объявлений переменных. Далее, создаётся узел типа
GOTO_EXPR
, который является выражением перехода на метку, т.е.
отображает языковое выражение goto
. Операндом данного узла будет
объявление метки, на которую переходим. Третьим элементом перехода будет узел
типа LABEL_EXPR
, единственным операндом которого является также
объявление метки, по которой совершается переход.
Оператор ветвления if-else
Теперь мы, наконец, можем посмотреть как создаётся оператор ветвления:
Код в gcc | Аналог на Си | GENERIC |
---|---|---|
//--------------------- Пример 7
|
if( var2 )
|
--------------- | VAR_DECL |-------------------------- | var2 | | --------------- | | | ... --------------------------|----------- | / | | --------------- --------------- | | | LABEL_DECL |--| GOTO_EXPR | | | | true_label | | true_label |\ | | --------------- --------------- \--------------- | | | COND_EXPR | | --------------- --------------- /--------------- | | LABEL_DECL |--| GOTO_EXPR |/ | | | false_label | | false_label | --------------- / --------------- --------------- | LABEL_EXPR |/ | \ | true_label | --------------- \ --------------- | LABEL_DECL |----------------- | | end_if | \ \ --------------- --------------- \ \| GOTO_EXPR | \ \ | endif_label | \ \ --------------- \ \ | \ \ --------------- \ ----------| LABEL_EXPR | \ | false_label | \ --------------- \ | \ --------------- -----------| LABEL_EXPR | | end_if | --------------- |
Получилось довольно страшно, разберём подробнее что здесь произошло. Как мы
помним, условный оператор в зависимости от результата выражения переводит
исполнение программы в истинную или ложную ветку. Это означает что нам
понадобится три выражения перехода. Первые два, как можно догадаться - это
переход на истинную ветвь true_label
, а второй - переход на ложную
ветвь false_label
. Третий переход будет безусловным, он нужен чтобы
после окончания одной из ветвей, которая будет располагаться выше в списке
выражений, мы не начинали исполнять другую ветвь, а через неё перепрыгнули на
участок сразу после условного выражения.
Теперь к самому условному выражению. Это узел с типом COND_EXPR
и
тремя операндами. Первым операндом будет выражение, которое даст истинный (т.е.
ненулевой) или ложный (т.е. нулевой) результат. Вторым операндом будет выражение
перехода на истинную ветвь исполнения, а третьим - соответственно на ложную.
Часто нам может понадобиться дополнительный лексический блок для каждой из
веток, его можно добавить рассмотренным ранее способом. В пример он не попал
чтобы не загромождать кодом и не запутывать читателя лишний раз.
Цикл do-while
Вообще говоря, создание циклов можно было бы и не рассматривать, т.к. оно почти
ничем не отличается от условных операторов, но для полноты обзора покажем как
создать канонический цикл do-while
:
Код в gcc | Аналог на Си | GENERIC |
---|---|---|
//--------------------- Пример 8
|
do
|
--------------- | VAR_DECL |------------------------------------- | var2 | | --------------- ------------------ | | | | | ... | --------------- | | | | LABEL_EXPR | | --------------- --------------- | loop_head | | | LABEL_DECL |--| GOTO_EXPR | --------------- | | loop_head | | loop_head | | | --------------- --------------- \--------------- / | | COND_EXPR |/ --------------- --------------- /--------------- | LABEL_DECL |--| GOTO_EXPR |/ | | loop_exit | | loop_exit | --------------- --------------- --------------- /| LABEL_EXPR | \ / | loop_exit | ------------------------ --------------- |
Можно увидеть что цикл получился даже проще чем обычное сравнение. Цикл состоит из двух меток, двух переходов и одного сравнения. Разница со сравнением в том что метка по истиной ветке перехода ведёт на код перед этим сравнением, т.е. по сути "зацикливает" путь исполнения.
Разумеется, для цикла for
код будет выглядеть сложнее, но идея
останется ровно такой же. Поэтому выполнение других видов циклов в этой статье
приводиться не будет.
Заключение
В этой статье мы рассмотрели основные языковые конструкции, встречающиеся внутри процедуры. При необходимости можно посмотреть эталонный код с примерами. Видно что хотя код создания простых языковых конструкций получается довольно объёмным, идейно он очень прост. В следующей статье будут рассмотрены вопросы создания и вызова функций.