目录
-
1. 引言 -
2. 整体架构概览 -
3. 预处理与词法分析 -
4. 语法分析:构建 AST -
5. 语义分析:名称解析与类型检查 -
6. 模板实例化机制 -
7. MLIR 方言与下降通道 -
8. 一个完整示例:编译模板函数 -
9. 总结与展望
1. 引言 📖
在编译器技术日新月异的今天,构建一个能处理复杂语言(如 C++)的编译器前端仍然是一项极具挑战性的工程。C++ 语言本身的复杂性——模板、概念、RAII、异常、复杂的名称查找规则等——要求编译器前端必须具备高度模块化、可扩展且性能优异的设计。
本文介绍了一个名为 cxx 的开源 C/C++ 编译器前端项目,其代码仓库位于
cplusplus[1] 。
该项目并非完整的工具链,而是一个专注于前端处理的库,能够将 C/C++ 源代码解析为抽象语法树(AST),执行完整的语义分析(包括类型检查、重载决议、模板实例化),并最终降级到 MLIR(Multi-Level Intermediate Representation)——一种由 LLVM 社区开发的现代化中间表示框架。
✨ 项目核心特性
通过剖析 cxx 的架构,我们可以一窥现代编译器的内部工作原理,理解如何将复杂的语言构造逐步转化为可用于优化和代码生成的形式。本文将从宏观架构入手,逐步深入核心模块,最后用具体示例串联整个处理流程。
2. 整体架构概览 🏗️
cxx 采用经典的多阶段流水线设计,每个阶段负责特定的变换,且阶段之间通过明确定义的数据结构(如 Token 流、AST、符号表)进行通信。下图展示了其整体架构:
核心阶段说明:
横向基础设施:
3. 预处理与词法分析 🔍
3.1 预处理器(Preprocessor)⚙️
Preprocessor 负责处理 #include、宏展开、条件编译、行拼接等。与常见一次性展开的预处理器不同,cxx 的预处理器支持异步文件解析——当遇到 #include 时,它不会立即打开文件,而是返回一个待处理状态,由外部(如文件系统或 JavaScript 运行时)提供内容后再继续。
这种协程式设计使得 Preprocessor 可以轻松集成到异步环境中(如 WebAssembly 中的 JavaScript 文件加载),也便于实现模块化的文件缓存和依赖追踪。预处理器维护了一个状态机,状态包括:正常处理中、遇到 #include 待处理、遇到 __has_include 待求值、需要读取文件内容等。
宏展开过程中,预处理器会处理:
-
• 函数式宏的参数替换 -
• 字符串化操作符( #) -
• 标记粘贴操作符( ##) -
• __VA_ARGS__可变参数 -
• 自递归和循环递归检测以防止无限展开
3.2 词法分析器(Lexer)📝
Lexer 将字符流转换为 Token 流,支持 C 和 C++ 两种语言模式。它处理:
Lexer 的 next() 方法返回 TokenKind,并记录每个 Token 的位置、前缀空格、行首标志等信息,供后续诊断和代码美化使用。
4. 语法分析:构建 AST 🌳
4.1 递归下降解析器 🧩
Parser 使用递归下降法,每个语法规则对应一个专门的解析函数。解析过程会不断消费 Token,并根据当前 Token 类型决定调用哪个解析函数,逐步构建 AST 节点。
递归下降解析器的优势在于:
-
• 可读性强:每个语法规则直接对应一个函数,代码结构与语法规则高度一致 -
• 易于调试:可以精确控制每个语法单元的解析过程 -
• 灵活扩展:添加新语法特性只需增加对应的解析函数
所有 AST 节点继承自 AST 基类,采用类型安全的访问者模式(ASTVisitor)进行遍历和操作。AST 节点使用 Arena 分配器(Arena)集中管理,生命周期与 TranslationUnit 一致,大幅减少内存碎片并提升分配效率 🧠。
4.2 AST 层次结构图
AST 层次结构说明:
每个 AST 节点都包含 firstSourceLocation() 和 lastSourceLocation() 方法,方便定位源码位置 📍。整个 AST 采用访问者模式设计,外部工具可以通过继承 ASTVisitor 并重写对应的 visit 方法来遍历和处理特定类型的节点,无需修改 AST 类本身。
5. 语义分析:名称解析与类型检查 🧐
5.1 Binder – 名称解析与符号表构建 🔗
Binder 的作用是将 AST 中的标识符与符号(Symbol)关联。符号表示一切具有名称的实体(变量、函数、类、枚举等)。Binder 维护当前作用域(ScopeSymbol*),在遍历 AST 时:
-
• 遇到 IdExpressionAST,查找当前作用域中的符号并设置ast->symbol -
• 遇到 ClassSpecifierAST,创建ClassSymbol并添加到作用域 -
• 遇到 FunctionDefinitionAST,创建FunctionSymbol并处理参数
符号表采用开放寻址哈希表(每个 ScopeSymbol 有 buckets_ 和 members_),支持快速名称查找 ⚡。
5.2 符号层次结构图
符号层次说明:
5.3 TypeChecker – 类型检查与隐式转换 ✅
TypeChecker 对 AST 进行类型标注,为每个 ExpressionAST 设置 type 和 valueCategory(lvalue/xvalue/prvalue)。其核心任务包括:
经过 TypeChecker 后,AST 中每个表达式都带有完整类型信息,并可能插入 ImplicitCastExpressionAST 节点以表示隐式转换 🎯。
6. 模板实例化机制
C++ 模板的实例化是编译器最复杂的部分之一。cxx 中的 ASTRewriter 类负责执行这一任务:它克隆原始模板的 AST,根据提供的模板参数替换模板形参占位符,生成特化后的 AST 并重新进行类型检查。
6.1 整体流程
模板实例化的完整流程如下:
流程说明:
6.2 模板参数替换(Substitution)
Substitution 类负责将 AST 中的模板形参替换为实参。替换是在 ASTRewriter 遍历 AST 时,按深度(depth)和索引(index) 定位模板形参的。例如,TypeParameterType 包含 depth 和 index,用于区分不同模板层级和位置。
6.3 偏特化匹配
类模板偏特化通过 tryPartialSpecialization 实现匹配。编译器会收集所有偏特化声明,逐个检查参数模式是否匹配,然后根据特化程度(score)选择最优的那一个;若 score 相同则报歧义错误。
评分维度(score 越高越特化):
6.4 函数模板参数推导
函数模板从调用实参推导模板参数,主要规则如下:
6.5 实例化缓存机制
模板实例化代价高昂,cxx 通过符号的 specializations_ 字段缓存已实例化的特化。每个模板符号维护一个特化列表,每条记录包含模板参数和对应的特化符号。当需要实例化时,首先在缓存中查找匹配项;若命中则直接返回,避免重复劳动;若未命中则执行完整实例化流程并将结果加入缓存供后续重用。
7. MLIR 方言与下降通道 🔽
7.1 自定义 MLIR 方言:CxxDialect 🎨
为了在 MLIR 中保留 C++ 的高级语义,项目定义了一个自定义方言 mlir::cxx::CxxDialect。该方言提供了与 C++ 语言构造相对应的操作和类型。
CxxDialect 类型层次图
类型映射关系(C++ Type → CxxDialect Type):
7.2 Codegen – 从 AST 到 MLIR 的映射流程 🔄
cxx::Codegen 类负责将类型化 AST 转换为 mlir::ModuleOp。转换过程中会维护一个符号→MLIR值的映射表,以及作用域信息。
Codegen 映射说明:
7.3 完整下降通道(Multi-Pass Lowering)📉
并不是直接从 CxxDialect 跳到 LLVM IR,而是要经过多个中间方言层,利用 MLIR 的 Pass 框架逐步剥离高级语义。
各 Pass 的转换清单:
该下降管线完全由 MLIR 框架支持,cxx 只需调用相应的 Pass 管理器即可完成整个下降过程 🚀。
8. 一个完整示例:编译模板函数 🧪
为了直观感受整个流程,我们用一个简单的 C++ 模板函数例子来展示从源码到 MLIR 的演变。
8.1 全流程综合图
全流程阶段说明:
8.2 各阶段代码展示 💻
8.2.1 源代码
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
return max(3, 5);
}
8.2.2 预处理与词法分析
预处理器展开 max 模板定义,但不会展开模板实例化。词法分析器生成 Token 流,进入解析。
8.2.3 解析生成 AST
Parser 生成包含 TemplateDeclarationAST(包含 FunctionDefinitionAST)和 FunctionDefinitionAST(main)的 AST。此时 AST 中的模板参数尚未绑定具体类型。
8.2.4 Binder 与 TypeChecker
-
• Binder将max模板声明中的T绑定为TypeParameterSymbol -
• TypeChecker对main中的max(3, 5)进行重载决议,发现max是一个模板,需要实例化。由于调用实参为int,推导出T = int
8.2.5 模板实例化(ASTRewriter)
-
• 调用 ASTRewriter::instantiate(max_template_symbol, {int}) -
• Substitution将模板定义中的所有T替换为int类型 -
• 生成一个 FunctionDefinitionAST对应于int max(int a, int b)
实例化后的 AST 类似于(简化):
int max(int a, int b) {
return (a > b) ? a : b;
}
8.2.6 Codegen 生成 MLIR(CxxDialect)
Codegen 遍历实例化后的 AST,生成:
cxx.func @max(%arg0: !cxx.int, %arg1: !cxx.int) -> !cxx.int {
%0 = cxx.binary gt %arg0, %arg1 : !cxx.int
%1 = cxx.select %0, %arg0, %arg1 : !cxx.int
cxx.return %1
}
cxx.func @main() -> !cxx.int {
%0 = cxx.call @max(3, 5) : (!cxx.int, !cxx.int) -> !cxx.int
cxx.return %0
}
8.2.7 下降至 Func/Arith/SCF 方言
func.func @max(%arg0: i32, %arg1: i32) -> i32 {
%cmp = arith.cmpi sgt, %arg0, %arg1 : i32
%sel = arith.select %cmp, %arg0, %arg1 : i32
func.return %sel
}
func.func @main() -> i32 {
%call = func.call @max(3, 5) : (i32, i32) -> i32
func.return %call
}
8.2.8 下降至 LLVM 方言
llvm.func @max(i32, i32) -> i32 {
// LLVM 指令
}
llvm.func @main() -> i32 {
// 调用 @max
}
8.2.9 导出为 LLVM IR
define i32 @max(i32 %a, i32 %b) {
%cmp = icmp sgt i32 %a, %b
%sel = select i1 %cmp, i32 %a, i32 %b
ret i32 %sel
}
define i32 @main() {
%call = call i32 @max(i32 3, i32 5)
ret i32 %call
}
9. 总结与展望 🎯
9.1 项目亮点 ✨
9.2 未来可能的扩展 🔮
-
• 支持更多 C++ 特性(协程、概念、模块) -
• 增加优化 Pass(常量传播、死代码消除)于 MLIR 层 -
• 提供 LSP 服务,实现语法高亮、补全、诊断 -
• 支持 WASM 等新兴目标平台
通过本文的梳理,我们可以看到 cxx 不仅是一个教学级的编译器前端,更是一个可投入实际使用的工业级框架。其清晰的架构和对现代编译技术的拥抱(MLIR)使其在研究、教学和产品化方面都极具价值。希望本文能为读者理解编译器内部工作提供有益的参考 🎉。
📄 许可证
本项目采用 MIT 许可证。
Copyright (c) 2026 Roberto Raggi roberto.raggi@gmail.com[2]
特此授予任何获得本软件及相关文档文件(“软件”)副本的人免费使用、复制、修改、合并、发布、分发、再许可和/或出售软件副本的权利,并允许软件的接收方也享有上述权利,但须满足以下条件:
上述版权声明和本许可声明应包含在软件的所有副本或实质性部分中。
本软件按“原样”提供,不作任何明示或暗示的保证,包括但不限于适销性、特定用途适用性和非侵权性的保证。在任何情况下,作者或版权持有人均不对因使用本软件而产生的任何索赔、损害或其他责任负责,无论是合同诉讼、侵权行为还是其他原因,即使已被告知可能发生此类损害。
引用链接
[1] cplusplus: https://github.com/robertoraggi/cplusplus[2] roberto.raggi@gmail.com: mailto:roberto.raggi@gmail.com

