我正在使用C语言实现一个简单玩具语言的编译器,我已经有了工作中的扫描器和解析器,并且对AST(抽象语法树)的概念函数/构建有一定的背景。我的问题与在C中表示AST的特定方式有关。我在不同的在线文本/资源中经常遇到三种风格:
每种节点类型一个结构体。
这个方法有一个基本节点"class"(结构体),是所有子结构体中的第一个字段。基本节点包含一个枚举,用于存储节点的类型(常量、二元运算符、赋值等)。结构体的成员使用一组宏访问,每个结构体都有一组宏。它看起来像这样:
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;
每个节点的布局只有一个结构体。
这似乎与上面的布局大致相同,除了不再有 ast_node_add 和 ast_node_assign,而是使用 ast_node_binary 来表示两者,因为这两个结构体的布局相同,它们仅由 base->class 的内容不同。这样做的好处似乎是一组更统一的宏(对于所有具有左右子节点的节点,都使用 LEFT(node)宏,而不是每对节点都有一个宏),但缺点是 C 类型检查将不会非常有用(例如,无法检测应该只有 ast_node_add 的情况下出现了 ast_node_assign)。
只有一个结构体总共,并使用联合来保存不同类型的节点数据。
比我能给出的更好的解释可以在这里找到。使用前面示例中的类型,它看起来像:
struct ast_node {
enum { CONSTANT, ADD, SUB, ASSIGNMENT } class;
union { int value;
struct { struct ast_node* left;
struct ast_node* right; } op;
};
我更倾向于第三个选项,因为它使递归遍历更加容易(避免了大量指针转换,而是使用联合体),但它也没有利用C类型检查。第一个选项似乎最危险,它依赖于将结构体指针强制转换为访问任何节点的成员(甚至是相同节点的不同成员需要不同的情况来访问(基础 vs. 左)),但这些强制转换是经过类型检查的,所以可能无关紧要。对我来说,第二个选项似乎是两个世界中最糟糕的选择,尽管也许我漏掉了什么。这三种方案中哪一种最好,为什么?是否有第四种更好的选择我还不知道?我假设它们都不是“一刀切”的解决方案,如果有影响的话,那么我的实现语言是一种静态类型的命令式语言,几乎是C的一个小子集。
对于第三个(联合)布局,我有一个具体问题。如果我仅使用value字段,那么会在value后面留下空白以容纳写入op的可能性吗?