从源码到AST:chibicc编译器如何解析C语言语法

从源码到AST:chibicc编译器如何解析C语言语法

【免费下载链接】chibicc A small C compiler 【免费下载链接】chibicc 项目地址: 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;
}

解析器的整体工作流程如下:

  1. 词法分析器(位于tokenize.c)将源代码转换为token流
  2. 预处理器(位于preprocess.c)处理宏展开和#include指令
  3. 语法解析器(位于parse.c)将token流转换为AST
  4. 代码生成器(位于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_scopeleave_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;
}
  1. 解析函数声明:declspec识别返回类型intfunction函数(parse.c#L156)处理函数名和参数列表,创建函数对象。

  2. 解析参数列表:func_params函数(parse.c#L584)解析int aint b,创建两个参数变量并添加到当前作用域。

  3. 解析函数体:compound_stmt函数(parse.c#L723)处理函数体的复合语句,进入新的作用域。

  4. 解析return语句:stmt函数识别return关键字,调用expr函数解析返回表达式a + b

  5. 解析表达式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的解析器是一个绝佳的学习案例。你可以从以下方面进一步探索:

  1. 扩展解析器支持更多C语言特性(如结构体、联合体、指针运算等)
  2. 实现更详细的错误检查和提示
  3. 添加语法高亮或代码格式化功能
  4. 开发基于AST的静态分析工具

通过深入理解chibicc的语法解析与AST构建技术,你不仅能掌握编译器前端的核心原理,还能将这些知识应用到代码分析、静态检查、重构工具等多种开发场景中。

参考资料

  • chibicc源代码:parse.ctokenize.ccodegen.c
  • 递归下降解析:一种直观的语法分析方法
  • C语言标准(ISO/IEC 9899):定义C语言语法和语义
  • 《编译器设计:原理、技术与工具》(龙书):深入了解编译原理

【免费下载链接】chibicc A small C compiler 【免费下载链接】chibicc 项目地址: https://gitcode.com/gh_mirrors/ch/chibicc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值