从源码到AST:chibicc编译器如何解析C语言语法
【免费下载链接】chibicc A small C compiler 项目地址: https://gitcode.com/gh_mirrors/ch/chibicc
编译器前端的核心挑战
你是否好奇C语言代码是如何被计算机理解的?当你写下int main() { return 0; }这样简单的代码时,编译器需要完成一系列复杂的转换。作为一款轻量级C编译器,chibicc通过精巧的语法解析器将源代码转换为抽象语法树(AST),这是编译器理解代码逻辑的关键步骤。本文将带你深入chibicc的语法解析与AST构建过程,揭示编译器如何将文本形式的代码转换为结构化的数据表示。
解析器架构概览
chibicc的语法解析器采用递归下降分析法实现,主要代码位于parse.c文件中。这种解析方法通过模拟语法规则的递归结构来构建解析树,非常适合C语言这样的上下文无关文法。
解析器的核心入口是declspec函数(parse.c#L381),它负责解析声明说明符,包括类型关键字、存储类说明符等。例如,当遇到int x = 5;这样的声明时,declspec会识别int作为基本类型,并将其传递给后续的声明器处理。
// 简化的declspec函数逻辑
static Type *declspec(Token **rest, Token *tok, VarAttr *attr) {
Type *ty = ty_int; // 默认类型为int
int counter = 0;
while (is_typename(tok)) {
// 处理类型关键字(void, char, int等)
// 处理存储类说明符(static, extern等)
// 处理类型限定符(const, volatile等)
tok = tok->next;
}
*rest = tok;
return ty;
}
解析器的整体工作流程如下:
- 词法分析器(位于tokenize.c)将源代码转换为token流
- 预处理器(位于preprocess.c)处理宏展开和#include指令
- 语法解析器(位于parse.c)将token流转换为AST
- 代码生成器(位于codegen.c)将AST转换为汇编代码
从Token到AST节点
AST(抽象语法树)是源代码的结构化表示,每个节点对应一个语法构造。chibicc定义了多种AST节点类型,如变量引用(ND_VAR)、数值常量(ND_NUM)、二元运算(ND_ADD、ND_SUB等)等。
基础节点构建
primary函数(parse.c#L753)负责解析基本表达式,包括标识符、常量、括号表达式等。当遇到标识符时,它会查找符号表并创建ND_VAR类型的节点:
static Node *primary(Token **rest, Token *tok) {
if (equal(tok, "(")) {
Node *node = expr(&tok, tok->next);
*rest = skip(tok, ")");
return node;
}
if (tok->kind == TK_IDENT) {
// 查找变量
VarScope *sc = find_var(tok);
if (!sc) error_tok(tok, "undefined variable");
Node *node = new_var_node(sc->var, tok);
*rest = tok->next;
return node;
}
if (tok->kind == TK_NUM) {
Node *node = new_num(tok->val, tok);
*rest = tok->next;
return node;
}
error_tok(tok, "expected expression");
}
表达式解析
表达式解析采用算符优先分析法,通过一系列函数层层递进处理不同优先级的运算符。从高优先级到低优先级依次为:
primary:基本表达式(标识符、常量、括号表达式)unary:一元运算符(+、-、*、&等)mul:乘法类运算符(*、/、%)add:加法类运算符(+、-)shift:移位运算符(<<、>>)relational:关系运算符(<、>、<=、>=)equality:相等性运算符(==、!=)bitand:按位与(&)bitxor:按位异或(^)bitor:按位或(|)logand:逻辑与(&&)logor:逻辑或(||)conditional:条件运算符(?:)assign:赋值运算符(=、+=、-=等)
以加法表达式解析为例,add函数(parse.c#L712)会调用mul函数解析乘法表达式作为操作数,然后处理连续的加法运算符:
static Node *add(Token **rest, Token *tok) {
Node *node = mul(rest, tok);
while (true) {
if (equal(*rest, "+")) {
Token *op = *rest;
Node *right = mul(rest, (*rest)->next);
node = new_binary(ND_ADD, node, right, op);
} else if (equal(*rest, "-")) {
Token *op = *rest;
Node *right = mul(rest, (*rest)->next);
node = new_binary(ND_SUB, node, right, op);
} else {
break;
}
}
return node;
}
这种结构确保了运算符优先级的正确处理,例如a + b * c会被解析为a + (b * c)而不是(a + b) * c。
声明解析与符号表管理
C语言的声明语法相对复杂,chibicc通过declarator函数(parse.c#L715)处理声明器,包括指针、数组、函数等复杂声明。
变量声明处理
declaration函数(parse.c#L716)协调处理声明说明符和声明器,最终创建变量对象并添加到符号表:
static Node *declaration(Token **rest, Token *tok, Type *basety, VarAttr *attr) {
// 解析声明器列表
while (!equal(tok, ";")) {
if (i++ > 0) tok = skip(tok, ",");
Type *ty = declarator(&tok, tok, basety);
char *name = get_ident(ty->name);
// 创建变量对象
Obj *var = new_lvar(name, ty);
// 处理初始化器
if (equal(tok, "=")) {
Node *init = lvar_initializer(&tok, tok->next, var);
// 添加初始化代码
}
}
*rest = tok->next;
return node;
}
符号表实现
chibicc使用哈希表(hashmap.c)实现符号表,用于存储变量、函数、类型等标识符信息。Scope结构体(parse.c#L32)表示一个作用域,包含变量表和标签表:
typedef struct Scope Scope;
struct Scope {
Scope *next; // 指向上一级作用域
HashMap vars; // 变量符号表
HashMap tags; // 结构体/联合体/枚举标签表
};
enter_scope和leave_scope函数(parse.c#L163-L171)用于管理作用域的进入和退出,实现了变量的作用域规则。
控制流语句解析
控制流语句(if、for、while等)的解析由stmt函数(parse.c#L724)协调处理。以if语句为例:
static Node *stmt(Token **rest, Token *tok) {
if (equal(tok, "if")) {
tok = tok->next;
tok = skip(tok, "(");
Node *cond = expr(&tok, tok);
tok = skip(tok, ")");
Node *then = stmt(&tok, tok);
Node *els = NULL;
if (equal(tok, "else"))
els = stmt(&tok, tok->next);
*rest = tok;
return new_if(cond, then, els);
}
// 处理其他语句类型(for, while, return等)
}
这种结构直接映射了if语句的语法结构,创建对应的ND_IF类型AST节点,包含条件表达式、then分支和else分支。
AST构建实例分析
让我们通过一个简单的C函数来看AST的构建过程:
int add(int a, int b) {
return a + b;
}
-
解析函数声明:
declspec识别返回类型int,function函数(parse.c#L156)处理函数名和参数列表,创建函数对象。 -
解析参数列表:
func_params函数(parse.c#L584)解析int a和int b,创建两个参数变量并添加到当前作用域。 -
解析函数体:
compound_stmt函数(parse.c#L723)处理函数体的复合语句,进入新的作用域。 -
解析return语句:
stmt函数识别return关键字,调用expr函数解析返回表达式a + b。 -
解析表达式
a + b:primary解析a,创建ND_VAR节点add函数解析+运算符primary解析b,创建ND_VAR节点- 创建ND_ADD节点,将两个ND_VAR节点作为操作数
最终生成的AST结构如下:
ND_FUNC
├── name: "add"
├── return_ty: int
├── params: [a(int), b(int)]
└── body: ND_BLOCK
└── ND_RETURN
└── ND_ADD
├── ND_VAR(a)
└── ND_VAR(b)
这个AST将被传递给代码生成器,最终转换为汇编代码。
错误处理机制
chibicc的解析器包含基本的错误处理机制,通过error_tok函数报告语法错误,并指出错误位置:
// 在parse.c中多次使用的错误处理模式
static Type *declarator(Token **rest, Token *tok, Type *ty) {
if (!equal(tok, "(")) {
error_tok(tok, "expected '(' in declarator");
}
// ...
}
这种即时错误检查有助于在解析过程中尽早发现语法错误,提供更准确的错误位置信息。
总结与扩展
chibicc的语法解析器展示了如何将复杂的C语言语法规则转换为高效的递归下降解析器。通过模块化的函数设计和清晰的AST节点结构,解析器能够处理C语言的大部分语法构造,包括变量声明、函数定义、控制流语句等核心特性。
对于希望深入了解编译器原理的开发者,chibicc的解析器是一个绝佳的学习案例。你可以从以下方面进一步探索:
- 扩展解析器支持更多C语言特性(如结构体、联合体、指针运算等)
- 实现更详细的错误检查和提示
- 添加语法高亮或代码格式化功能
- 开发基于AST的静态分析工具
通过深入理解chibicc的语法解析与AST构建技术,你不仅能掌握编译器前端的核心原理,还能将这些知识应用到代码分析、静态检查、重构工具等多种开发场景中。
参考资料
- chibicc源代码:parse.c、tokenize.c、codegen.c
- 递归下降解析:一种直观的语法分析方法
- C语言标准(ISO/IEC 9899):定义C语言语法和语义
- 《编译器设计:原理、技术与工具》(龙书):深入了解编译原理
【免费下载链接】chibicc A small C compiler 项目地址: https://gitcode.com/gh_mirrors/ch/chibicc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



