第 2 章 编译器的结构 - 学习笔记
本文为《Learn LLVM 17》第 2 章的学习笔记,主要介绍编译器前端的构建块,并通过 calc 算术表达式语言实现一个完整的编译器前端。
1. 编译器的构建块
编译器通常分为三个部分:
| 部分 | 职责 | 说明 |
|---|---|---|
| 前端 | 源语言 → IR | 处理源语言,生成中间表示 |
| 中端 | IR 转换与优化 | 提高性能、减少代码体积 |
| 后端 | IR → 机器码 | 生成目标平台机器码 |
LLVM 的优势:LLVM 核心库提供了中端和后端,我们只需专注于前端开发。
前端的处理流程
|
|
- 词法分析器:将源代码分解为标记 (Token),如数字、标识符、操作符
- 解析器:分析标记的句法结构,生成抽象语法树 (AST)
- 语义分析器:检查语言规则(如变量声明、类型兼容性)
- 代码生成:将 AST 转换为 LLVM IR
2. 算术表达式语言 calc
2.1 示例
|
|
- 变量用
with声明 - 程序会询问变量值并输出计算结果
2.2 EBNF 语法
|
|
语法对编译器作者的意义:
- 定义所有标记 → 指导词法分析器实现
- 语法规则 → 可转化为解析器
- 作为正确性的衡量标准
3. 词法分析 (Lexer)
3.1 核心组件
- Token:唯一编号 + 文本内容
- Lexer:将文本输入转换为 Token 序列
3.2 关键实现要点
LLVM 工具类:
llvm::MemoryBuffer:只读内存块,末尾有\0llvm::StringRef:封装指针+长度,无需\0结尾
字符分类:不使用 <cctype>,原因:
- 受 locale 影响(如德语变音符)
char→int转换有移植性问题
Token 类型:eoi, unknown, ident, number, comma, colon, plus, minus, star, slash, l_paren, r_paren, KW_with
关键字识别:标识符正则也匹配关键字,先收集字符再判断是否为 with
4. 语法分析 (Parser)
4.1 递归下降解析器
- 每个非终结符对应一个解析方法
- 可选组
(…)?→if判断 - 重复组
(…)*→while循环 - 选择
|→switch或if-else
4.2 错误处理:恐慌模式
- 丢弃标记直到找到可继续的符号
- 对每个非终结符定义后继标记集
- 简单但有效:开发者修复第一条错误后重新编译
4.3 LL(1) 语法
递归下降解析器要求语法满足 LL(1) 条件,大多数网上语法不满足。经典参考:《编译原理》(龙书)。
5. 抽象语法树 (AST)
5.1 设计原则
- 去除对语义无意义的符号(如
with、,、:) - 只保留:变量列表、表达式结构、操作符
5.2 AST 类层次
|
|
5.3 访问者模式
ASTVisitor:定义visit()接口- 各 AST 节点实现
accept(Visitor)调用对应visit - 便于语义分析和代码生成时遍历树
6. 语义分析 (Sema)
6.1 检查内容
- 变量在使用前必须声明
- 变量不能重复声明
6.2 实现思路
- 使用
llvm::StringSet<>存储已声明变量 WithDecl:将变量加入集合,检查重复Factor(Ident):检查变量是否在集合中BinaryOp:递归检查左右子树
7. 代码生成 (CodeGen)
7.1 LLVM IR 文本表示
库函数声明:
|
|
字符串常量:
|
|
main 函数结构:
|
|
注意:LLVM 16+ 默认使用不透明指针 ptr,不再使用 i8* 等类型化指针。
7.2 ToIRVisitor 实现要点
IRBuilder<>:构建 IR 指令StringMap<Value*>:变量名 →calc_read返回值WithDecl:为每个变量创建字符串常量,调用calc_read,存入 mapFactor:Ident 查 map,Number 转ConstantIntBinaryOp:CreateNSWAdd/Sub/Mul、CreateSDiv
8. 驱动与运行时
8.1 Calc 驱动程序流程
|
|
8.2 运行时库 (rtcalc.c)
calc_write(int):printf输出结果calc_read(char*):fgets+sscanf读取整数,非法输入则exit(1)
9. 构建与测试
9.1 CMake 配置
find_package(LLVM REQUIRED CONFIG)llvm_map_components_to_libnames(llvm_libs Core)- 源文件:
Calc.cpp,CodeGen.cpp,Lexer.cpp,Parser.cpp,Sema.cpp
9.2 完整编译流程
|
|
10. 总结
本章通过 calc 算术表达式语言,完整实现了编译器前端的四个阶段:
- 词法分析:手写 Lexer,Token 枚举 +
StringRef - 语法分析:递归下降 Parser,恐慌模式错误恢复
- 语义分析:访问者模式 +
StringSet作用域检查 - 代码生成:ToIRVisitor 生成 LLVM IR,配合 llc 生成机器码
关键收获:
- 语法 (EBNF) 直接指导词法/语法分析实现
- 访问者模式统一 AST 遍历逻辑
- LLVM 提供 IR 接口,只需实现前端即可利用其优化与后端