大数跨境

从源码到 MLIR:一个现代C++编译器的前端设计与实现

从源码到 MLIR:一个现代C++编译器的前端设计与实现 ai算法芯片与系统
2026-07-25
6
导读:cxx 是一个开源 C++ 编译器前端,支持 C++26/C23,提供完整的语法分析 API,涵盖预处理、解析、语义分析、模板实例化,并可通过 MLIR 降级生成 LLVM IR,支持多语言扩展。

 

目录

  1. 1. 引言
  2. 2. 整体架构概览
  3. 3. 预处理与词法分析
  4. 4. 语法分析:构建 AST
  5. 5. 语义分析:名称解析与类型检查
  6. 6. 模板实例化机制
  7. 7. MLIR 方言与下降通道
  8. 8. 一个完整示例:编译模板函数
  9. 9. 总结与展望

1. 引言 📖

在编译器技术日新月异的今天,构建一个能处理复杂语言(如 C++)的编译器前端仍然是一项极具挑战性的工程。C++ 语言本身的复杂性——模板、概念、RAII、异常、复杂的名称查找规则等——要求编译器前端必须具备高度模块化、可扩展且性能优异的设计。

本文介绍了一个名为 cxx 的开源 C/C++ 编译器前端项目,其代码仓库位于
cplusplus[1] 。

该项目并非完整的工具链,而是一个专注于前端处理的库,能够将 C/C++ 源代码解析为抽象语法树(AST),执行完整的语义分析(包括类型检查、重载决议、模板实例化),并最终降级到 MLIR(Multi-Level Intermediate Representation)——一种由 LLVM 社区开发的现代化中间表示框架。

✨ 项目核心特性

特性
说明
语法分析 API
提供扫描、预处理、解析和检查源代码语法的完整接口,适用于静态分析、代码生成等各类工具。
多语言支持
除了 C/C++,该库还提供了 TypeScript 和 JavaScript 的语法分析 API,可用于跨语言工具链。
前沿标准支持
紧跟语言演进,支持 C++26 和 C23 的最新语言特性(如 _BitIntconstexpr 增强、C++ 模块等)。
开源许可
采用 MIT 许可证,允许自由使用、修改和分发,无论是开源还是闭源项目均可放心集成。

通过剖析 cxx 的架构,我们可以一窥现代编译器的内部工作原理,理解如何将复杂的语言构造逐步转化为可用于优化和代码生成的形式。本文将从宏观架构入手,逐步深入核心模块,最后用具体示例串联整个处理流程。


2. 整体架构概览 🏗️

cxx 采用经典的多阶段流水线设计,每个阶段负责特定的变换,且阶段之间通过明确定义的数据结构(如 Token 流、AST、符号表)进行通信。下图展示了其整体架构:

核心阶段说明:

阶段
输入
输出
关键组件
预处理
源文件
Token 流
Preprocessor
词法分析
源文件
Token 流
Lexer
语法分析
Token 流
原始 AST
Parser
名称绑定
原始 AST
带符号的 AST
Binder
类型检查
带符号的 AST
类型化 AST
TypeChecker
模板实例化
类型化 AST
实例化后 AST
ASTRewriter
MLIR 生成
实例化后 AST
MLIR 模块
Codegen
下降与代码生成
MLIR 模块
LLVM IR
MLIR Passes

横向基础设施:

组件
职责
Control
 🏭
全局对象工厂,创建类型、符号、字面量、名称等,保证唯一性和去重
DiagnosticsClient
 🩺
错误、警告、备注的报告接口,支持错误计数和错误限制
MemoryLayout
 📐
描述目标平台的类型大小和对齐方式,用于布局计算

3. 预处理与词法分析 🔍

3.1 预处理器(Preprocessor)⚙️

Preprocessor 负责处理 #include、宏展开、条件编译、行拼接等。与常见一次性展开的预处理器不同,cxx 的预处理器支持异步文件解析——当遇到 #include 时,它不会立即打开文件,而是返回一个待处理状态,由外部(如文件系统或 JavaScript 运行时)提供内容后再继续。

这种协程式设计使得 Preprocessor 可以轻松集成到异步环境中(如 WebAssembly 中的 JavaScript 文件加载),也便于实现模块化的文件缓存和依赖追踪。预处理器维护了一个状态机,状态包括:正常处理中、遇到 #include 待处理、遇到 __has_include 待求值、需要读取文件内容等。

宏展开过程中,预处理器会处理:

  • • 函数式宏的参数替换
  • • 字符串化操作符(#
  • • 标记粘贴操作符(##
  • • __VA_ARGS__ 可变参数
  • • 自递归和循环递归检测以防止无限展开

3.2 词法分析器(Lexer)📝

Lexer 将字符流转换为 Token 流,支持 C 和 C++ 两种语言模式。它处理:

语言特性
示例
编码前缀 u8"..."
u"..."U"..."L"..."
原始字符串字面量 R"delim(...)delim"
数字分隔符 1'000'000
(C++14)
转义序列 \n
\x1F\u1234\U0001F4A9

Lexer 的 next() 方法返回 TokenKind,并记录每个 Token 的位置、前缀空格、行首标志等信息,供后续诊断和代码美化使用。


4. 语法分析:构建 AST 🌳

4.1 递归下降解析器 🧩

Parser 使用递归下降法,每个语法规则对应一个专门的解析函数。解析过程会不断消费 Token,并根据当前 Token 类型决定调用哪个解析函数,逐步构建 AST 节点。

递归下降解析器的优势在于:

  • • 可读性强:每个语法规则直接对应一个函数,代码结构与语法规则高度一致
  • • 易于调试:可以精确控制每个语法单元的解析过程
  • • 灵活扩展:添加新语法特性只需增加对应的解析函数

所有 AST 节点继承自 AST 基类,采用类型安全的访问者模式(ASTVisitor)进行遍历和操作。AST 节点使用 Arena 分配器(Arena)集中管理,生命周期与 TranslationUnit 一致,大幅减少内存碎片并提升分配效率 🧠。

4.2 AST 层次结构图

AST 层次结构说明:

基类
子类示例
用途
UnitAST TranslationUnitAST
ModuleUnitAST
表示整个翻译单元或模块 📦
DeclarationAST SimpleDeclarationAST
FunctionDefinitionASTTemplateDeclarationAST
各种声明 📜
StatementAST IfStatementAST
ForStatementASTCompoundStatementAST
语句 🔄
ExpressionAST BinaryExpressionAST
CallExpressionASTIdExpressionAST
表达式 ➕
SpecifierAST NamedTypeSpecifierAST
AutoTypeSpecifierAST
类型说明符 🏷️

每个 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 符号层次结构图

符号层次说明:

符号类型
用途
ScopeSymbol
作用域基类,包含成员列表和查找方法
ClassSymbol
类/结构体/联合体,包含基类、构造函数、成员
FunctionSymbol
函数/方法,包含参数、重载、模板信息
VariableSymbol
变量,包含类型、初始化器、常量值
TemplateParametersSymbol
模板参数列表,用于模板声明

5.3 TypeChecker – 类型检查与隐式转换 ✅

TypeChecker 对 AST 进行类型标注,为每个 ExpressionAST 设置 type 和 valueCategory(lvalue/xvalue/prvalue)。其核心任务包括:

任务
说明
类型推导 auto
 类型、数组大小推导、返回类型推导
隐式转换
标准转换(整型提升、浮点提升、指针转换)和用户定义转换(构造函数、转换运算符)
重载决议 OverloadResolution
 基于候选函数集和参数类型选择最佳可行函数
运算符重载解析
对 +-*[] 等内建运算符,查找类中定义的 operator 函数
常量表达式求值
利用 ASTInterpreter 在编译期求值,用于 static_assert、模板非类型参数等

经过 TypeChecker 后,AST 中每个表达式都带有完整类型信息,并可能插入 ImplicitCastExpressionAST 节点以表示隐式转换 🎯。


6. 模板实例化机制

C++ 模板的实例化是编译器最复杂的部分之一。cxx 中的 ASTRewriter 类负责执行这一任务:它克隆原始模板的 AST,根据提供的模板参数替换模板形参占位符,生成特化后的 AST 并重新进行类型检查。

6.1 整体流程

模板实例化的完整流程如下:

流程说明:

步骤
说明
A
编译器遇到模板名称(如 max<int>(1,2) 或 std::vector<int>
B-C
检查是否有 <...> 显式参数,有则收集
D
若无显式参数,从函数实参类型推导(如 max(1,2) → T=int
E
推导失败则报错(如 max("hello", 5) 类型不匹配)
G-H
检查 C++20 的 requires 子句(若存在)
I
SFINAE 环境下静默失败,否则报错
J-K
查找 specializations_ 缓存,命中则直接返回
M-N
克隆模板 AST,将 TypeParameterType 等占位符替换为实际类型/值
O
对新生成的 AST 重新执行类型检查(可能触发更多实例化)
P
将新符号加入特化缓存供后续重用

6.2 模板参数替换(Substitution)

Substitution 类负责将 AST 中的模板形参替换为实参。替换是在 ASTRewriter 遍历 AST 时,按深度(depth)和索引(index) 定位模板形参的。例如,TypeParameterType 包含 depth 和 index,用于区分不同模板层级和位置。

模板形参
实参
替换后的效果
typename T int
所有 TypeParameterType 变为 IntType
T a int a
参数类型变为 int
T b int b
参数类型变为 int
T + T int + int
表达式类型变为 int

6.3 偏特化匹配

类模板偏特化通过 tryPartialSpecialization 实现匹配。编译器会收集所有偏特化声明,逐个检查参数模式是否匹配,然后根据特化程度(score)选择最优的那一个;若 score 相同则报歧义错误。

评分维度(score 越高越特化):

评分维度
说明
精确类型匹配
实参与模式完全一致(如 int 匹配 int
嵌套模板匹配
匹配嵌套模板的参数(如 vector<int>
非类型参数匹配
常量表达式匹配(如 N=5
包参数匹配
参数包展开的匹配
推导参数匹配
成功推导出的参数数量
模板参数数量
偏特化中包参数的个数(越少越特化)

6.4 函数模板参数推导

函数模板从调用实参推导模板参数,主要规则如下:

规则
示例
说明
转发引用
template<typename T> void f(T&&)
若实参是左值,T 推导为 T&;若实参是右值,T 推导为非引用
参数包
template<typename... T> void f(T... args)
实参列表展开填充包参数
引用折叠
T& &
 → T&
处理 T&& 与 T& 的组合
SFINAE
推导失败不算错误
替换失败不会立即报错,会尝试其他重载

6.5 实例化缓存机制

模板实例化代价高昂,cxx 通过符号的 specializations_ 字段缓存已实例化的特化。每个模板符号维护一个特化列表,每条记录包含模板参数和对应的特化符号。当需要实例化时,首先在缓存中查找匹配项;若命中则直接返回,避免重复劳动;若未命中则执行完整实例化流程并将结果加入缓存供后续重用。


7. MLIR 方言与下降通道 🔽

7.1 自定义 MLIR 方言:CxxDialect 🎨

为了在 MLIR 中保留 C++ 的高级语义,项目定义了一个自定义方言 mlir::cxx::CxxDialect。该方言提供了与 C++ 语言构造相对应的操作和类型。

CxxDialect 类型层次图

类型映射关系(C++ Type → CxxDialect Type):

C++ Type
CxxDialect Type
void !cxx.void
int !cxx.int
double !cxx.double
int* !cxx.pointer<!cxx.int>
int& !cxx.lvalue_ref<!cxx.int>
std::vector<int> !cxx.class<std::vector, !cxx.int>
int(int, double) !cxx.func<!cxx.int, !cxx.int, !cxx.double>

7.2 Codegen – 从 AST 到 MLIR 的映射流程 🔄

cxx::Codegen 类负责将类型化 AST 转换为 mlir::ModuleOp。转换过程中会维护一个符号→MLIR值的映射表,以及作用域信息。

Codegen 映射说明:

AST 节点类型
生成的 MLIR 操作
TranslationUnitAST mlir::ModuleOp
FunctionDefinitionAST cxx.func
CompoundStatementAST cxx.block
ReturnStatementAST cxx.return
IfStatementAST cxx.if
ForStatementAST cxx.for
BinaryExpressionAST cxx.binary
CallExpressionAST cxx.call
CastExpressionAST cxx.cast

7.3 完整下降通道(Multi-Pass Lowering)📉

并不是直接从 CxxDialect 跳到 LLVM IR,而是要经过多个中间方言层,利用 MLIR 的 Pass 框架逐步剥离高级语义。

各 Pass 的转换清单:

Pass 名称
源操作 → 目标操作
ConvertCxxToFuncArithSCF cxx.func
 → func.funccxx.call → func.callcxx.binary → arith.addi/mulf/cmpicxx.if → scf.ifcxx.var → func.alloca + memref
ConvertFuncToLLVM func.func
 → llvm.funcfunc.call → llvm.callfunc.return → llvm.return
ConvertArithToLLVM arith.addi
 → llvm.addarith.cmpi → llvm.icmparith.select → llvm.select
ConvertSCFToLLVM scf.if
 → llvm 分支;scf.for → llvm.br + llvm.cond_br
mlir-translate llvm
 方言 → LLVM IR 字符串

该下降管线完全由 MLIR 框架支持,cxx 只需调用相应的 Pass 管理器即可完成整个下降过程 🚀。


8. 一个完整示例:编译模板函数 🧪

为了直观感受整个流程,我们用一个简单的 C++ 模板函数例子来展示从源码到 MLIR 的演变。

8.1 全流程综合图

全流程阶段说明:

阶段
输入
输出
说明
源码
模板定义 + main
文本
用户编写的 C++ 代码
预处理+词法
源码
Token 流
宏展开、Token 化
解析
Token 流
模板 AST + main AST
构建抽象语法树
语义分析
AST
带类型的 AST
类型检查、重载决议
模板实例化
模板 AST
实例化后的 AST
替换 T → int
Codegen
类型化 AST
CxxDialect MLIR
生成高层 MLIR
下降
CxxDialect
LLVM IR
逐步下降到 LLVM

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)和 FunctionDefinitionASTmain)的 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 项目亮点 ✨

亮点
说明
模块化设计
 🧩
每个阶段职责清晰,易于替换或扩展
类型系统完整
 📚
支持 C++ 的大部分类型,包括类、引用、指针、数组、函数、成员指针、模板参数等
模板实例化
 ⚡
完整实现函数模板参数推导、偏特化匹配、SFINAE
现代化中间表示
 🚀
选择 MLIR 作为后端,充分利用其优化和代码生成生态
协程式预处理器
 🔄
文件解析与外部 I/O 解耦,适应多种运行环境
多语言支持
 🌐
除 C/C++ 外,还支持 TypeScript 和 JavaScript
前沿标准
 📅
支持 C++26 和 C23 的最新语言特性

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

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 216
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读3.7k
粉丝0
内容216