Я реализую компилятор для простого игрушечного языка в C. У меня есть рабочий сканер и парсер, и разумный фон для концептуальной функции/построения АСТ. Мой вопрос связан с конкретным способом представления AST в C. Я часто встречал три стиля в разных текстах/ресурсах в Интернете:
Одна структура для типа node.
У этого есть базовый node "class" (struct), который является первым полем во всех дочерних структурах. База node содержит перечисление, которое хранит тип node (постоянный, двоичный оператор, назначение и т.д.). Доступ к элементам структуры осуществляется с помощью набора макросов с одним набором для каждой структуры. Это выглядит примерно так:
struct ast_node_base {
enum {CONSTANT, ADD, SUB, ASSIGNMENT} class;
};
struct ast_node_constant {
struct ast_node_base *base;
int value;
};
struct ast_node_add {
struct ast_node_base *base;
struct ast_node_base *left;
struct ast_node_base *right;
};
struct ast_node_assign {
struct ast_node_base *base;
struct ast_node_base *left;
struct ast_node_base *right;
};
#define CLASS(node) ((ast_node_base*)node)->class;
#define ADD_LEFT(node) ((ast_node_add*)node)->left;
#define ADD_RIGHT(node) ((ast_node_add*)node)->right;
#define ASSIGN_LEFT(node) ((ast_node_assign*)node)->left;
#define ASSIGN_RIGHT(node) ((ast_node_assign*)node)->right;
Одна структура на макет node.
Похоже, что это в основном такое же, как в приведенном выше макете, за исключением того, что вместо ast_node_add и ast_node_assign у него будет атрибут ast_node_binary для представления обоих, поскольку макет двух структур одинаковый, и они отличаются только содержимым базы → класс. Преимуществом этого является более равномерный набор макросов (LEFT (node) для всех узлов с левым и правым, а не с одной парой макросов на), но недостатком кажется, что проверка типа C не будет (не было бы способа обнаружить ast_node_assign, где, например, должен быть только ast_node_add).
Одна общая структура, с объединением для хранения различных типов данных node.
Лучшее объяснение этого, чем я могу дать, можно найти здесь. Используя типы из предыдущего примера, это будет выглядеть так:
struct ast_node {
enum { CONSTANT, ADD, SUB, ASSIGNMENT } class;
union { int value;
struct { struct ast_node* left;
struct ast_node* right; } op;
};
Я склонен больше любить третий вариант, потому что он делает рекурсивный обход намного проще (в том, что многие литья указателей избегают в пользу объединения), но он также не использует проверку типа C. Первый вариант кажется самым опасным в том, что он полагается на указатели на структурированные объекты, чтобы получить доступ к члену любого node (даже разные члены одного и того же node, требующие доступа к различным случаям (базовые и левые)), но эти приведения проверяются по типу, поэтому могут быть спорными. Второй вариант для меня кажется худшим из обоих миров, хотя, может быть, я чего-то не хватает.
Какие из этих трех схем являются лучшими и почему? Есть ли лучший четвертый вариант, с которым я еще не сталкивался? Я предполагаю, что ни один из них не является решением "одного размера подходит всем", поэтому, если он имеет значение для языка, который я реализую, это статически типизированный императив язык, почти небольшое подмножество C.
Конкретный вопрос, который у меня есть о третьем (объединении) макете. Если я использую только поле значений, будет ли пустое пространство, следующее за значением, чтобы разместить возможность записи op на?