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

Вступление

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

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

Работа с функциями

Объявление функции

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

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

// Создаём узел, представляющий тип const char *
tree cnst_char_tree = build_pointer_type (
    build_qualified_type (char_type_node,
                          TYPE_QUAL_CONST));

tree * printf_params = new tree[1]; // Массив, содержащий параметры
// Создаём параметр функции
printf_params[0] = cnst_char_tree;

// Создаём тип функции "int (const char *, ...)"
tree printf_fndecl_type = build_varargs_function_type_array (
    integer_type_node, // Тип возврата
    1,                 // Количество аргументов
    printf_params);    // Массив с типами аргументов
delete[] printf_params;

// Создаём объявление функции "int printf(const char *, ...);"
tree printf_fndecl = build_fn_decl(
    "printf",            // Символьное имя функции
    printf_fndecl_type); // Тип функции

// Говорим что это объявление используется и его нельзя удалять
DECL_PRESERVE_P (printf_fndecl) = 1;
// Функция printf не должна иметь признака external
DECL_EXTERNAL (printf_fndecl)   = 0;

// <-------------- Сюда будем добавлять код
int printf(const int char *, ...);
---------------------------
|      FUNCTION_DECL      |
|        "printf"         |
| int (const char *, ...) |
---------------------------

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

Передача аргументов и вызов функции

А вот вызовом функций мы пока не занимались, попробуем что-либо напечатать:

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

// Строим строковый литерал
tree hello_str = build_string_literal (
    8,           // Длина строки
    "Hello!\n"); // Строка

// Строим выражение вызова
tree call_stm = build_call_expr (
    printf_fndecl, // Узел с вызываемой функцией
    1,             // Количество аргументов
    hello_str);    // Узлы с аргументами

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

// <-------------- Сюда будем добавлять код
printf("Hello!\n");
                             ---------------------------
                             |         "Hello"         |
--------------------------- /---------------------------
|          CALL           |/
|        "printf"         |
---------------------------

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

Передача аргументов по указателю

Ещё одним важным элементом для вызова функции является передача аргументов по указателю. Рассмотрим как это делается на примере вызова функции scanf.

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

tree * scanf_params = new tree[1]; // Массив, содержащий параметры
// Создаём параметр функции
scanf_params[0] = cnst_char_tree;

// Создаём тип функции "int (const char *, ...)"
tree scanf_fndecl_type = build_varargs_function_type_array (
    integer_type_node, // Тип возврата
    1,                 // Количество аргументов
    scanf_params);     // Массив с типами аргументов
delete[] scanf_params;

// Создаём объявление функции "int scanf(const char *, ...);"
tree scanf_fndecl = build_fn_decl(
    "scanf",            // Символьное имя функции
    scanf_fndecl_type); // Тип функции

// Говорим что это объявление используется и его нельзя удалять
DECL_PRESERVE_P (scanf_fndecl) = 1;
// Функция scanf не должна иметь признака external
DECL_EXTERNAL (scanf_fndecl)   = 0;

// Строим строковый литерал с форматной строкой
tree read_num_format = build_string_literal (
    3,       // Длина строки
    "%d\0"); // Строка

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

// Указываем контекст переменной
DECL_CONTEXT (var_decl_1)     = main_fndecl;
// Отмечаем что на переменную берётся адрес
TREE_ADDRESSABLE (var_decl_1) = 1;

// Строим узел взятия адреса переменной
tree addr_expr = build1 (
    ADDR_EXPR,                              // Тип узла
    build_pointer_type (integer_type_node), // Тип выражения
    var_decl_1);                            // Адресуемая переменная

// Строим выражение вызова scanf
tree call_stm_2 = build_call_expr (
    scanf_fndecl,    // Узел с вызываемой функцией
    2,               // Количество аргументов
    read_num_format, // Узлы с аргументами
    addr_expr);      // Узлы с аргументами

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

// Строим строковый литерал с форматной строкой
tree print_num_format = build_string_literal (
    9,            // Длина строки
    "Num: %d\n"); // Строка

// Строим выражение вызова
tree call_stm_3 = build_call_expr (
    printf_fndecl,    // Узел с вызываемой функцией
    2,                // Количество аргументов
    print_num_format, // Узлы с аргументами
    var_decl_1);

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

int var_decl_1;
scanf("%d", &var_decl_1);
printf("Num: %d\n", var_decl_1);
---------------------------
|      FUNCTION_DECL      |
|         "scanf"         |
| int (const char *, ...) |         -----------------   ---------------
---------------------------         |     "%d"      |   |  VAR_DECL   |
            \                      /-----------------  /|    var1     |
       ---------------------------/                   / |    int      |
       |          CALL           |  -----------------/  ---------------
       |         "scanf          |--|   ADDR_EXPR   |    /
       ---------------------------  ------------------- /
                   |                                   /
       --------------------------- /------------------/
       |          CALL           |/
       |        "printf"         |\ -----------------
       --------------------------- \|  "Num %d\n"   |
                                    -----------------

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

Заключение

Это была последняя статья из серии работы с GENERIC в gcc. Эталонный код для неё можно взять здесь. Рассмотренного материала вполне хватит чтобы научить лицевую часть собственного компилятора взаимодействовать с gcc и использовать его в качестве оптимизатора и кодогенератора.