在C语言中表示抽象语法树

35

我正在使用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的可能性吗?

1
我认为你在句子“比我更好的解释可以在这里找到”中想要链接注释26,而不是注释25。 - Charles Victorio
1个回答

19

你可以让任何一个布局方式起作用。

我更喜欢联合布局,因为所有节点都有“相同”的布局。

[您可能会发现有一个“子列表”选项很有用,例如一个任意大小的动态数组子节点,而不是具有左倾或右倾列表。]

你会发现这个问题并不是构建编译器时最困难的问题。相反,它是拥有符号表、执行各种分析、选择机器级IR、构建代码生成器和进行代码优化所面临的挑战。然后你将遇到真正的用户,并发现你真正做错了什么 :-}

我会选择一种并运用它,这样你就有机会接近其他问题。


谢谢!这正是我想听到的,很高兴知道我还没有偏离轨道。 - user1547129
@user1547129:如果可能的话,避免使用父指针可能会很烦人,但也许是值得的。我认为在这个阶段你不会真正需要它们,但当你想要移动树并且必须取消链接和重新链接它们时,它们会引起头痛。 - user541686
1
@Mehrdad:所有的数据结构都很烦人,因为你必须确保它们(及其不变量)保持最新状态。你指出了一个权衡,在维护父指针和编码父列表缓存之间,以允许您在没有父链接时向上遍历树。 (我个人更喜欢父链接;涉及树的大多数代码都是检查它,这应该是最容易编写的。[注意:我构建执行大规模树转换的工具]。)你的情况可能有所不同。 - Ira Baxter
你选择的第二个是联合吗? - user15307601
我不明白你的问题。我的回答似乎已经清楚地表明了我更喜欢联合作为首选项。话虽如此,我的DMS系统以统一的方式处理50多种不同语言的AST,并基于使用频率具有几个基线节点布局。但它并不是每个非终端符号一个节点。我们使用终端节点(仅具有父链接、类型和值)、固定度数子节点(具有1到15个子节点)和列表节点,其具有一个父节点和一个动态数组的子节点来捕获列表。当您进行长期投资时,拥有几种节点类型是可以接受的。 - Ira Baxter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接