Работа с представлением GENERIC в gcc - часть 3

Вступление

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

Основные конструкции

Будем предполагать что из предыдущей статьи мы научились создавать лексические блоки, цепочки определений и списки выражений, и к началу этой части у нас уже есть простая функция main, которая возвращает 0. Т.е. будем считать что в файле x1.cc у нас уже содержится код из первых трёх примеров второй заметки. Весь дальнейший код будет добавляться между концом второго и началом третьего примера.

Объявление переменных

Мы уже писали код объявления переменных, но рассмотрим его ещё раз:

Код в gcc Аналог на Си GENERIC
//--------------------- Пример 1

tree var_decl_1 = build_decl (
  UNKNOWN_LOCATION,        // Положение в исходном коде
  VAR_DECL,                // Тип узла
  get_identifier("var1"),  // Имя переменной
  integer_type_node);      // Тип переменной

tree var_decl_2 = build_decl (
  UNKNOWN_LOCATION,        // Положение в исходном коде
  VAR_DECL,                // Тип узла
  get_identifier("var2"),  // Имя переменной
  boolean_type_node);      // Тип переменной

tree var_decl_3 = build_decl (
  UNKNOWN_LOCATION,        // Положение в исходном коде
  VAR_DECL,                // Тип узла
  get_identifier("var3"),  // Имя переменной
  float_type_node);        // Тип переменной

// Создаём цепочку объявлений
DECL_CHAIN (var_decl_1) = var_decl_2;
DECL_CHAIN (var_decl_2) = var_decl_3;

// Помечаем что переменные нельзя удалять
TREE_USED (var_decl_1)  = 1;
TREE_USED (var_decl_2)  = 1;
TREE_USED (var_decl_3)  = 1;

// <-------------- Сюда будем добавлять код
int var1;
bool var2;
float var3;
---------------
|  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

tree const_decl_1 = build_int_cst (
    integer_type_node, // Тип константы
    10);               // Значение

REAL_VALUE_TYPE r3; // Переменная с плавающим значением
real_from_string ( &r3,    // Переменная, куда запишем
                   "3.4"); // Значение
tree const_decl_3 = build_real (
    float_type_node, // Тип плавающего значения
    r3);             // Переменная, со значением

// Говорим что объявления инициализируются константами
DECL_INITIAL (var_decl_1) = const_decl_1;
DECL_INITIAL (var_decl_3) = const_decl_3;

// <-------------- Сюда будем добавлять код
int var1 = 10;
float var3 = 3.4;
---------------
|  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

// Создаём константу true
tree const_decl_2 = build_int_cst_type (boolean_type_node, 1);

// Присваиваем значение константы переменной
tree bool_assign = fold_build2 (
    MODIFY_EXPR,    // Тип узла
    void_type_node, // Тип выражения
    var_decl_2,     // Цель присваивания
    const_decl_2);  // Присваиваемое значение

// Добавляем выражение присваивания в список
append_to_statement_list (
    bool_assign, // Добавляемое выражение
    &stm_list);  // Список выражений

// <-------------- Сюда будем добавлять код
var2 = 1;
                 ---------------
                /|     var2    |
---------------/ ---------------
| MODIFY_EXPR |
---------------\ ---------------
                \|      1      |
                 ---------------

Для этого примера потребуется поднять наверх выражение:

// Список выражений
tree stm_list = alloc_stmt_list ();

В самом примере ничего нового не появилось, присваивание выполняет узел с типом MODIFY_EXPR, у выражения данного узла тип всегда void_type_node. К сожалению я не очень понял почему он создаётся при помощи функции fold_build2, но так делают все, и мы будем. Первым операндом выражения является адресат присвоения значения, вторым - само присваиваемое значение.

Арифметика

Более интересный пример с арифметическими выражениями:

Код в gcc Аналог на Си GENERIC
//--------------------- Пример 4

// Создаём константу
tree const_decl_4 = build_int_cst_type (integer_type_node, 3);

// Выражение умножения
tree mult_tree = build2 (
    MULT_EXPR,         // Тип узла
    integer_type_node, // Тип выражения
    var_decl_1,        // Левый операнд
    const_decl_4);     // Правый операнд

// Выражение умножения
tree minus_tree = build2 (
    MINUS_EXPR,        // Тип узла
    integer_type_node, // Тип выражения
    mult_tree,         // Левый операнд
    const_decl_1);     // Правый операнд

// Присваиваем значение выражения переменной
tree var1_assign = fold_build2 (
    MODIFY_EXPR,    // Тип узла
    void_type_node, // Тип выражения
    var_decl_1,     // Цель присваивания
    minus_tree);    // Присваиваемое значение

// Добавляем выражение присваитвания в список
append_to_statement_list (
    var1_assign, // Добавляемое выражение
    &stm_list);  // Список выражений

// <-------------- Сюда будем добавлять код
var1 = var1 * 3 - 10;
                 ---------------                   ---------------
                /|     var1    |                  /|    var1     |
---------------/ ---------------  ---------------/ ---------------
| MODIFY_EXPR |                  /|  MULT_EXPR  |
---------------\ ---------------/ ---------------\ ---------------
                \|  MINUS_EXPR |                  \|      3      |
                 ---------------\ ---------------  ---------------
                                 \|     10      |
                                  ---------------

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

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

Выражение возврата

С ним мы уже сталкивались не раз, теперь наконец разберём их:

Код в gcc Аналог на Си GENERIC
//--------------------- Пример 5

// Присваиваем значение переменной var_decl_1
// переменной возврата
tree ret_assign2 = fold_build2 (
    MODIFY_EXPR,     // Тип узла
    void_type_node,  // Тип выражения
    resdecl,         // Цель присваивания
    var_decl_1);     // Присваиваемое значение

// Здесь создаём узел return
tree return_stm2 = build1 (
    RETURN_EXPR,    // Тип узла
    void_type_node, // Тип выражения
    ret_assign2);   // Возвращаемое выражение

// Добавляем выражение возврата в список
append_to_statement_list (
    return_stm2, // Добавляемое выражение
    &stm_list);  // Список выражений

// <-------------- Сюда будем добавлять код
return var1;
                                  ---------------
                                 /| RESULT_DECL |
---------------  ---------------/ ---------------
| MODIFY_EXPR |--| RETURN_EXPR |
---------------  ---------------\ ---------------
                                 \|    var1     |
                                  ---------------

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

Безусловный переход

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

Код в gcc Аналог на Си GENERIC
//--------------------- Пример 6

// Создаём определение метки
tree label_decl_1 = build_decl (
    UNKNOWN_LOCATION,           // Положение в исходном коде
    LABEL_DECL,                 // Тип узла
    get_identifier ("label_1"), // Узел с именем переменной
    void_type_node);            // Тип выражения

// Указываем контекст для метки
DECL_CONTEXT (label_decl_1) = main_fndecl;
// Добавляем в цепочку определений
DECL_CHAIN (var_decl_3) = label_decl_1;

// Создаём выражение goto
tree goto_1 = build1 (
    GOTO_EXPR,      // Тип узла
    void_type_node, // Тип выражения
    label_decl_1);  // Метка, на которую переходим

// Строим саму метку
tree label_expr_1 = build1 (
    LABEL_EXPR,     // Тип узла
    void_type_node, // Тип выражения
    label_decl_1);  // Объявление метки

// Добавляем выражение перехода
append_to_statement_list (
    goto_1,     // Добавляемое выражение
    &stm_list); // Список выражений

// Сюда для эксперимента можно добавить выражений
// которые мы перепрыгнем и не исполним

// Добавляем выражение метки, в которую переходим
append_to_statement_list (
    label_expr_1, // Добавляемое выражение
    &stm_list);   // Список выражений

// <-------------- Сюда будем добавлять код
goto label_1;
label_1:
                 ---------------
                /|  GOTO_EXPR  |
---------------/ ---------------
| LABEL_DECL  |
---------------\ ---------------
                \| LABEL_EXPR  |
                 ---------------

Конструкция goto состоит из трёх основных элементов. Во-первых это узел типа LABEL_DECL с объявлением метки. Это объявление добавляется в цепочку объявлений переменных. Далее, создаётся узел типа GOTO_EXPR, который является выражением перехода на метку, т.е. отображает языковое выражение goto. Операндом данного узла будет объявление метки, на которую переходим. Третьим элементом перехода будет узел типа LABEL_EXPR, единственным операндом которого является также объявление метки, по которой совершается переход.

Оператор ветвления if-else

Теперь мы, наконец, можем посмотреть как создаётся оператор ветвления:

Код в gcc Аналог на Си GENERIC
//--------------------- Пример 7

// Создаём определение метки
tree true_label_decl = build_decl (
    UNKNOWN_LOCATION,              // Положение в исходном коде
    LABEL_DECL,                    // Тип узла
    get_identifier ("true_label"), // Узел с именем переменной
    void_type_node);               // Тип выражения

// Указываем контекст для метки
DECL_CONTEXT (true_label_decl) = main_fndecl;
// Добавляем в цепочку определений
DECL_CHAIN (label_decl_1) = true_label_decl;

// Создаём выражение goto
tree goto_true = build1 (
    GOTO_EXPR,        // Тип узла
    void_type_node,   // Тип выражения
    true_label_decl); // Метка, на которую переходим

// Создаём определение метки
tree false_label_decl = build_decl (
    UNKNOWN_LOCATION,               // Положение в исходном коде
    LABEL_DECL,                     // Тип узла
    get_identifier ("false_label"), // Узел с именем переменной
    void_type_node);                // Тип выражения

// Указываем контекст для метки
DECL_CONTEXT (false_label_decl) = main_fndecl;
// Добавляем в цепочку определений
DECL_CHAIN (true_label_decl) = false_label_decl;

// Создаём выражение goto
tree goto_false = build1 (
    GOTO_EXPR,         // Тип узла
    void_type_node,    // Тип выражения
    false_label_decl); // Метка, на которую переходим

// Создаём определение метки
tree endif_label_decl = build_decl (
    UNKNOWN_LOCATION,          // Положение в исходном коде
    LABEL_DECL,                // Тип узла
    get_identifier ("end_if"), // Узел с именем переменной
    void_type_node);           // Тип выражения

// Указываем контекст для метки
DECL_CONTEXT (endif_label_decl) = main_fndecl;
// Добавляем в цепочку определений
DECL_CHAIN (false_label_decl) = endif_label_decl;

// Создаём выражение goto
tree goto_endif = build1 (
    GOTO_EXPR,         // Тип узла
    void_type_node,    // Тип выражения
    endif_label_decl); // Метка, на которую переходим

// Создаём узел с условным выражением
tree cond_expr = build3 (
    COND_EXPR,      // Тип узла
    void_type_node, // Тип выражения
    var_decl_2,     // Выражение условия
    goto_true,      // Переход на истинную ветку
    goto_false);    // Переход на ложную ветку

// Добавляем выражение сравнения
append_to_statement_list (
    cond_expr,  // Добавляемое выражение
    &stm_list); // Список выражений

// Строим метку начала истинного пути
tree true_label_expr = build1 (
    LABEL_EXPR,       // Тип узла
    void_type_node,   // Тип выражения
    true_label_decl); // Объявление метки

// Добавляем выражение метки, в которую переходим
append_to_statement_list (
    true_label_expr, // Добавляемое выражение
    &stm_list);      // Список выражений

// Здесь идёт код истинной ветки
// Можно добавить узел BIND для создания
// нового лексического блока

// Добавляем выражение перехода на конец if'а
append_to_statement_list (
    goto_endif, // Добавляемое выражение
    &stm_list); // Список выражений

// Строим метку начала ложного пути
tree false_label_expr = build1 (
    LABEL_EXPR,        // Тип узла
    void_type_node,    // Тип выражения
    false_label_decl); // Объявление метки

// Добавляем выражение метки, в которую переходим
append_to_statement_list (
    false_label_expr, // Добавляемое выражение
    &stm_list);       // Список выражений

// Здесь идёт код ложной ветки
// Можно добавить узел BIND для создания
// нового лексического блока

// Строим метку начала конца условного выражения.
// Сюда мы прыгаем после окончания работы истиной ветки
tree endif_label_expr = build1 (
    LABEL_EXPR,        // Тип узла
    void_type_node,    // Тип выражения
    endif_label_decl); // Объявление метки

// Добавляем выражение метки, в которую переходим
append_to_statement_list (
    endif_label_expr, // Добавляемое выражение
    &stm_list);       // Список выражений

// <-------------- Сюда будем добавлять код
if( var2 )
{
  // На самом деле лексического блока в примере нет
} else
{
  // На самом деле лексического блока в примере нет
}
---------------
|   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

// Создаём определение метки
tree loop_head_decl = build_decl (
    UNKNOWN_LOCATION,             // Положение в исходном коде
    LABEL_DECL,                   // Тип узла
    get_identifier ("loop_head"), // Узел с именем переменной
    void_type_node);              // Тип выражения

// Указываем контекст для метки
DECL_CONTEXT (loop_head_decl) = main_fndecl;
// Добавляем в цепочку определений
DECL_CHAIN (endif_label_decl) = loop_head_decl;

// Создаём выражение goto
tree goto_loop_head = build1 (
    GOTO_EXPR,       // Тип узла
    void_type_node,  // Тип выражения
    loop_head_decl); // Метка, на которую переходим

// Создаём определение метки
tree loop_exit_decl = build_decl (
    UNKNOWN_LOCATION,             // Положение в исходном коде
    LABEL_DECL,                   // Тип узла
    get_identifier ("loop_exit"), // Узел с именем переменной
    void_type_node);              // Тип выражения

// Указываем контекст для метки
DECL_CONTEXT (loop_exit_decl) = main_fndecl;
// Добавляем в цепочку определений
DECL_CHAIN (loop_head_decl) = loop_exit_decl;

// Создаём переход на выход из цикла
tree goto_loop_exit = build1 (
    GOTO_EXPR,       // Тип узла
    void_type_node,  // Тип выражения
    loop_exit_decl); // Метка, на которую переходим

// Строим метку головы цикла
tree loop_head_expr = build1 (
    LABEL_EXPR,      // Тип узла
    void_type_node,  // Тип выражения
    loop_head_decl); // Объявление метки

// Добавляем выражение метки головы цикла
append_to_statement_list (
    loop_head_expr, // Добавляемое выражение
    &stm_list);     // Список выражений

// Здесь идёт код тела цикла
// Можно добавить узел BIND для создания
// нового лексического блока

// Создаём узел с условным выражением
tree cond_expr2 = build3 (
    COND_EXPR,       // Тип узла
    void_type_node,  // Тип выражения
    var_decl_2,      // Выражение условия
    goto_loop_head,  // Переход на истинную ветку
    goto_loop_exit); // Переход на ложную ветку

// Добавляем выражение сравнения
append_to_statement_list (
    cond_expr2, // Добавляемое выражение
    &stm_list); // Список выражений

// Строим метку выхода из цикла
tree loop_exit_expr = build1 (
    LABEL_EXPR,      // Тип узла
    void_type_node,  // Тип выражения
    loop_exit_decl); // Объявление метки

// Добавляем выражение метки выхода из цикла
append_to_statement_list (
    loop_exit_expr, // Добавляемое выражение
    &stm_list);     // Список выражений
do
{
  // На самом деле лексического блока в примере нет
} while( var2 );
---------------
|   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 код будет выглядеть сложнее, но идея останется ровно такой же. Поэтому выполнение других видов циклов в этой статье приводиться не будет.

Заключение

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