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

Вступление

Создание собственного языка программирования является довольно сложной задачей, и одним из способов упростить себе жизнь будет использование готовой инфраструктуры, предоставляемой существующими компиляторами. Например, достаточно просто написать лицевую часть компилятора, обеспечивающую разбор текста программы и перевести дерево разбора программы в формат, понимаемый gcc или llvm. По такому пути идут создатели как классических языков вроде Ada, Pascal или Fortran, так и стильных, модных, молодёжных вроде Rust или Go.

В этом цикле статей мы разберём примеры работы с промежуточным представлением GENERIC, которое используется создателями языков программирования для взаимодействия с gcc. Материалов по работе с GENERIC практически не существует. Самым ценным материалом является цикл статей «A tiny GCC front end». В нём описывается и настройка gcc и создание лексического, синтаксического и семантического анализаторов их взаимодействие с gcc. Цикл «A tiny GCC front end» был наиболее полезным ресурсом при подготовке данного материала.

Кроме того, стоит отметить устаревшую статью на эту же тему «GCC Frontend HOWTO». Официальная документация gcc довольно скромная и находится в нескольких местах. Во-первых основная документация, которая описывает некоторые понятия из GENERIC, а во-вторых это страничка wiki, в которой можно найти несколько полезных примеров.

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

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

Настройка и начало работы

Немного теории

Чтобы представлять как работает gcc, рассмотрим схему:

                 ---------------------------------------------------
                 |                       GCC                       |
-------------    | -------------    -------------    ------------- |    -------------
| Программа | -> | |  GENERIC  | -> |  GIMPLE   | -> |    RTL    | | -> |   Язык    |
|           |    | |           |    |           |    |           | |    | ассемблера|
-------------    | -------------    -------------    ------------- |    -------------
                 |                                                 |
                 ---------------------------------------------------

В данной схеме можно видеть что gcc принимает на вход программу (текст на языке программирования), на выходе даёт программу на языке ассемблера, а внутри содержит три части.

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

Представление GENERIC является наиболее высокоуровневым (т.е. близким к языку программирования) и представляет из себя что-то, похожее на дерево абстрактного синтаксиса. Именно в это представление и переводят программы для того чтобы gcc мог компилировать их дальше.

Скачивание и установка

Для начала скачаем исходники gcc. Это можно сделать либо через систему контроля версий, либо скачать стабильную версию с ftp сервера:

$ wget http://mirror.linux-ia64.org/gnu/gcc/releases/gcc-10.1.0/gcc-10.1.0.tar.xz

$ tar -xpvf gcc-10.1.0.tar.xz

$ cd gcc-10.1.0

$ ./contrib/download_prerequisites

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

$ mkdir obj bin

$ export GCC_PATH="`pwd`"

$ cd obj

$ ../configure --prefix=${GCC_PATH}/bin/ --enable-languages=c,c++ --enable-checking

$ make -j3 && make install

В этой последовательности команд интерес представляет только строчка конфигурирования. Рассмотрим её опции по отдельности. Опция --prefix=${GCC_PATH}/bin/ задаёт каталог установки, в который скопируются исполняемые файлы компилятора после запуска make install. Опция --enable-languages=c,c++ говорит что собирать мы будем только компиляторы C и C++. Тратить время на сборку остальных компиляторов сейчас смысла не имеет. Опция --enable-checking нужна разработчикам компиляторов для увеличения количества и информативности сообщений о внутренних ошибках компилятора.

Даже на достаточно мощных машинах сборка займёт ощутимое время, поэтому можно расслабиться и пойти попить чай. После её завершения имеет смысл провести простой тест на работоспособность компилятора:

$ cat > t.c
int main(void) { return 0; }

$ ${GCC_PATH}/bin/bin/gcc t.c

$ ./a.out

$ echo $?
0

Регистрация собственной лицевой части

Итак, будем считать что мы успешно проделали все предыдущие этапы и получили работающую систему. Теперь перейдём к регистрации нашей собственной лицевой части компилятора. Для этого рассмотрим содержимое каталога ${GCC_PATH}/gcc. Все файлы, исходного кода, которые лежат в нём являются частью компилятора. Кроме файлов в нём присутствуют каталоги с названиями языков программирования. В этих каталогах находятся лицевые части компилятора для соответствующих языков.

Чтобы создать свою лицевую часть, нам необходимо создать каталог с названием языка. Пусть в нашем случае это будет язык X:

$ mkdir ${GCC_PATH}/gcc/x

$ cd ${GCC_PATH}/gcc/x

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

Первым рассматриваемым файлом будет config-lang.in с описанием новой лицевой части:

# Название языка. Будет использоваться, например, в опции --enable-languages
language="x"

# Список исполняемых файлов, которые будут запускаться драйвером.
# Здесь имена компиляторов должны заканчиваться на \$(exeext)
compilers="x1\$(exeext)"

# Файлы, сканируемые сборщиком мусора
gtfiles="\$(srcdir)/x/x1.cc"

# Не собираем наш язык по умолчанию
build_by_default="no"

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

Далее необходимо показать gcc для каких файлов и в каком порядке вызывать компилятор. Это делается в файле lang-specs.h:

{".x", "@x", 0, 1, 0},
{"@x", "x1 %i %(cc1_options) %{!fsyntax-only:%(invoke_as)}", 0, 1, 0},

Ещё одним вспомогательным файлом будет файл xspec.cc:

/* Иногда требуется менять опции компилятора перед запуском обработчика входной
 * программы. В нашем случае не используется */
void
lang_specific_driver (struct cl_decoded_option ** /* in_decoded_options */,
                      unsigned int              * /* in_decoded_options_count */,
                      int                       * /*in_added_libraries */)
{
}

/* Вызывается перед компоновкой. Возвращает 0 в случае успеха и -1 в обратном */
int
lang_specific_pre_link (void)
{
  /* Не используем */
  return 0;
}

/* Количество дополнительных файлов, которые могут создаваться в lang_specific_pre_link */
int lang_specific_extra_outfiles = 0; /* Мы не создаём дополнительных файлов */

Выше мы могли заметить что для системы сборки мусора был указан файл x1.cc. Этот файл будет точкой входа в лицевую часть, код разбора текста программы будет (а в нашем случае просто создание кода) запускаться из функции x_langhook_parse_file. Файл придётся заполнить некоторым количеством заглушек, которые нам не нужны и которые мы не будем описывать:

#include "config.h"
#include "system.h"
#include "coretypes.h"
#include "target.h"
#include "tree.h"
#include "tree-iterator.h"
#include "input.h"
#include "diagnostic.h"
#include "stringpool.h"
#include "cgraph.h"
#include "gimplify.h"
#include "gimple-expr.h"
#include "convert.h"
#include "print-tree.h"
#include "stor-layout.h"
#include "fold-const.h"
#include "langhooks.h"
#include "langhooks-def.h"
#include "debug.h"

/* Language-dependent contents of a type. */

struct GTY (()) lang_type
{
  char dummy;
};

/* Language-dependent contents of a decl. */

struct GTY (()) lang_decl
{
  char dummy;
};

struct GTY(()) lang_identifier
{
  struct tree_identifier common;
};

/* The resulting tree type. */

union GTY ((desc ("TREE_CODE (&%h.generic) == IDENTIFIER_NODE"),
          chain_next ("CODE_CONTAINS_STRUCT (TREE_CODE (&%h.generic), "
          "TS_COMMON) ? ((union lang_tree_node *) TREE_CHAIN "
          "(&%h.generic)) : NULL"))) lang_tree_node
{
  union tree_node GTY ((tag ("0"), desc ("tree_node_structure (&%h)"))) generic;
  struct lang_identifier GTY ((tag ("1"))) identifier;
};

/* We don't use language_function. */

struct GTY (()) language_function
{
  int dummy;
};

/* Functions called directly by the generic backend. */

tree
convert (tree /* type */, tree /* expr */)
{
  gcc_unreachable ();
}

/**
* Эта функция запускается при подаче компилятору входного файла. Из неё мы
* начинаем наши действия
*/
static void
x_langhook_parse_file ()
{
  fprintf(stderr, "Привет!\n");
}

/* Language hooks. */

static bool
x_langhook_init (void)
{
  build_common_tree_nodes (false);

  build_common_builtin_nodes ();

  return true;
}

static tree
x_langhook_type_for_mode (enum machine_mode mode, int unsignedp)
{
  if (mode == TYPE_MODE (float_type_node))
    return float_type_node;

  if (mode == TYPE_MODE (double_type_node))
    return double_type_node;

  if (mode == TYPE_MODE (intQI_type_node))
    return unsignedp ? unsigned_intQI_type_node : intQI_type_node;
  if (mode == TYPE_MODE (intHI_type_node))
    return unsignedp ? unsigned_intHI_type_node : intHI_type_node;
  if (mode == TYPE_MODE (intSI_type_node))
    return unsignedp ? unsigned_intSI_type_node : intSI_type_node;
  if (mode == TYPE_MODE (intDI_type_node))
    return unsignedp ? unsigned_intDI_type_node : intDI_type_node;
  if (mode == TYPE_MODE (intTI_type_node))
    return unsignedp ? unsigned_intTI_type_node : intTI_type_node;

  if (mode == TYPE_MODE (integer_type_node))
    return unsignedp ? unsigned_type_node : integer_type_node;

  if (mode == TYPE_MODE (long_integer_type_node))
    return unsignedp ? long_unsigned_type_node : long_integer_type_node;

  if (mode == TYPE_MODE (long_long_integer_type_node))
    return unsignedp ? long_long_unsigned_type_node
        : long_long_integer_type_node;

  if (COMPLEX_MODE_P (mode))
  {
    if (mode == TYPE_MODE (complex_float_type_node))
      return complex_float_type_node;
    if (mode == TYPE_MODE (complex_double_type_node))
      return complex_double_type_node;
    if (mode == TYPE_MODE (complex_long_double_type_node))
      return complex_long_double_type_node;
    if (mode == TYPE_MODE (complex_integer_type_node) && !unsignedp)
      return complex_integer_type_node;
  }

  /* gcc_unreachable */
  return NULL;
}

/* Record a builtin function. We just ignore builtin functions. */

static tree
x_langhook_builtin_function (tree decl)
{
  return decl;
}

static bool
x_langhook_global_bindings_p (void)
{
  gcc_unreachable ();
  return true;
}

static tree
x_langhook_pushdecl (tree decl ATTRIBUTE_UNUSED)
{
  gcc_unreachable ();
}

static tree
x_langhook_getdecls (void)
{
  return NULL;
}

#undef LANG_HOOKS_NAME
#define LANG_HOOKS_NAME "X"

#undef LANG_HOOKS_INIT
#define LANG_HOOKS_INIT x_langhook_init

#undef LANG_HOOKS_PARSE_FILE
#define LANG_HOOKS_PARSE_FILE x_langhook_parse_file

#undef LANG_HOOKS_TYPE_FOR_MODE
#define LANG_HOOKS_TYPE_FOR_MODE x_langhook_type_for_mode

#undef LANG_HOOKS_BUILTIN_FUNCTION
#define LANG_HOOKS_BUILTIN_FUNCTION x_langhook_builtin_function

#undef LANG_HOOKS_GLOBAL_BINDINGS_P
#define LANG_HOOKS_GLOBAL_BINDINGS_P x_langhook_global_bindings_p

#undef LANG_HOOKS_PUSHDECL
#define LANG_HOOKS_PUSHDECL x_langhook_pushdecl

#undef LANG_HOOKS_GETDECLS
#define LANG_HOOKS_GETDECLS x_langhook_getdecls

struct lang_hooks lang_hooks = LANG_HOOKS_INITIALIZER;

#include "gt-x-x1.h"
#include "gtype-x.h"

Мы записали 4 файла с настройками и исходным кодом, и нам осталось только написать сценарий для сборки этих файлов. Он будет находиться в файле Make-lang.in:

GCCX_INSTALL_NAME := $(shell echo gccx|sed '$(program_transform_name)')
GCCX_TARGET_INSTALL_NAME := $(target_noncanonical)-$(shell echo gccx|sed '$(program_transform_name)')

# The name for selecting x in LANGUAGES.
x: x1$(exeext)

.PHONY: x

GCCX_OBJS = \
  $(GCC_OBJS) \
  x/xspec.o \
  $(END)

gccx$(exeext): $(GCCX_OBJS) $(EXTRA_GCC_OBJS) libcommon-target.a $(LIBDEPS)
	+$(LINKER) $(ALL_LINKERFLAGS) $(LDFLAGS) -o $@ \
	  $(GCCX_OBJS) $(EXTRA_GCC_OBJS) libcommon-target.a \
	  $(EXTRA_GCC_LIBS) $(LIBS)


x_OBJS = \
    x/x1.o

x1$(exeext): attribs.o $(x_OBJS) $(BACKEND) $(LIBDEPS)
	+$(LLINKER) $(ALL_LINKERFLAGS) $(LDFLAGS) -o $@ \
	      $(x_OBJS) attribs.o $(BACKEND) $(LIBS) $(BACKENDLIBS)

x.all.cross:

x.start.encap: gccx$(exeext)
x.rest.encap:

# Нет специфических тестов для языка (по крайней мере мы их делать не будем)
selftest-x:

x.install-common: installdirs
	-rm -f $(DESTDIR)$(bindir)/$(GCCX_INSTALL_NAME)$(exeext)
	$(INSTALL_PROGRAM) gccx$(exeext) $(DESTDIR)$(bindir)/$(GCCX_INSTALL_NAME)$(exeext)
	rm -f $(DESTDIR)$(bindir)/$(GCCX_TARGET_INSTALL_NAME)$(exeext); \
	( cd $(DESTDIR)$(bindir) && \
      $(LN) $(GCCX_INSTALL_NAME)$(exeext) $(GCCX_TARGET_INSTALL_NAME)$(exeext) ); \

# Required goals, they still do nothing
x.install-man:
x.install-info:
x.install-pdf:
x.install-plugin:
x.install-html:
x.info:
x.dvi:
x.pdf:
x.html:
x.man:
x.mostlyclean:
x.clean:
x.distclean:
x.maintainer-clean:

# make uninstall
x.uninstall:
	-rm -f gccx$(exeext) x1$(exeext)
	-rm -f $(x_OBJS)

# Used for handling bootstrap
x.stage1: stage1-start
	-mv x/*$(objext) stage1/x
x.stage2: stage2-start
	-mv x/*$(objext) stage2/x
x.stage3: stage3-start
	-mv x/*$(objext) stage3/x
x.stage4: stage4-start
	-mv x/*$(objext) stage4/x
x.stageprofile: stageprofile-start
	-mv x/*$(objext) stageprofile/x
x.stagefeedback: stagefeedback-start
	-mv x/*$(objext) stagefeedback/x

Это необходимый минимум для запуска собственной лицевой части. Теперь можно перенастроить и пересобрать gcc с нашим собственным модулем:

$ cd ${GCC_PATH}/obj/

$ make distclean

$ ../configure --prefix="${GCC_PATH}"/bin --enable-languages=c,c++,x --enable-checking

$ make -j3 && make install

Если этот этап завершился успешно, то попробуем запустить получившийся компилятор:

$ touch t.x

$ ${GCC_PATH}/bin/bin/gccx t.x -c
Привет!

Сообщение Привет! говорит нам о том что мы всё сделали правильно и можем переходить к заключительному этапу этой заметки.

Заключение

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