迷你编译器详细文档
一、编译器是什么?
1.1 编译器的作用
想象一下,你写了一段C++代码,但计算机只能理解0和1。编译器就是那个"翻译官",把你写的高级语言代码翻译成计算机能懂的机器语言。
你的代码 → 编译器 → 机器能执行的代码
1.2 学习编译器的好处
- 深入理解编程语言:了解代码背后发生了什么
- 提高调试能力:知道错误是怎么产生的
- 培养系统思维:学会将大问题分解成小问题
二、这个迷你编译器能做什么?
我们的编译器可以处理这样的简单代码:
c
main() {
a = 10 + 5 * 2; // 计算表达式
if (a > 20) { // 条件判断
b = a - 20;
} else {
b = 20 - a;
}
while (b < 100) { // 循环
b = b + 10;
}
return b; // 返回值
}
然后把它变成x86汇编代码,最终可以被计算机执行。
三、编译器的五个阶段(像工厂流水线)
我们的编译器像一条生产线,源代码经过5个车间变成最终产品:
源代码 → [词法分析] → [语法分析] → [中间代码] → [代码优化] → [目标代码]
四、详细分解每个模块
4.1 词法分析器(Scanner)- "拆零件车间"
4.1.1 它在做什么?
把一整段代码拆成一个个有意义的"单词",就像把一句话拆成单个词语。
输入 :a = 10 + 20;
输出 :[标识符"a", 运算符"=", 数字"10", 运算符"+", 数字"20", 分隔符";"]
4.1.2 为什么要这样做?
计算机需要知道每个部分的含义,就像人读文章需要知道每个词的意思。
4.1.3 核心代码详解
cpp
// 定义Token(单词)的结构
struct Token {
int type; // 类型编号(比如1代表main,10代表变量名)
string value; // 具体的值(比如"a"、"10")
int line; // 在第几行(用于错误提示)
};
类型编号的含义:
- 1-12:关键字(main、int、if、while等)
- 10:变量名
- 20:数字
- 21-36:运算符(+、-、*、/、>、<等)
- 0:结束标记
4.1.4 关键方法详解
1. recognizeIdentifier() - 识别变量名和关键字
cpp
Token recognizeIdentifier() {
string value;
char ch = getChar(); // 读取一个字符
if (isalpha(ch)) { // 如果是字母开头
value += ch;
// 继续读,直到遇到非字母数字
while (pos < length && isalnum(sourceCode[pos])) {
value += sourceCode[pos];
pos++;
}
}
// 检查是不是关键字
if (keywords.find(value) != keywords.end()) {
return Token(keywords[value], value, line); // 返回关键字
} else {
return Token(10, value, line); // 返回普通变量名
}
}
工作原理:
- 从第一个字母开始读取
- 一直读到不是字母或数字为止
- 检查读到的单词是不是关键字
- 返回相应的Token
2. recognizeNumber() - 识别数字
cpp
Token recognizeNumber() {
string value;
bool hasDot = false; // 有没有小数点
char ch = getChar();
// 处理正负号
if (ch == '+' || ch == '-') {
value += ch;
ch = getChar();
}
while (pos <= length) {
if (isdigit(ch)) {
value += ch; // 数字直接加入
} else if (ch == '.' && !hasDot) {
value += ch; // 第一个小数点加入
hasDot = true;
} else {
break; // 遇到其他字符就停止
}
ch = getChar();
}
return Token(20, value, line); // 返回数字Token
}
3. recognizeOperator() - 识别运算符
cpp
Token recognizeOperator() {
string value;
char ch = getChar();
value += ch;
// 检查是不是双字符运算符(比如>=、<=)
if (pos < length) {
char nextCh = sourceCode[pos];
string doubleOp = value + nextCh;
if (operators.find(doubleOp) != operators.end()) {
value = doubleOp;
pos++; // 多读一个字符
}
}
return Token(operators[value], value, line);
}
为什么要检查双字符?
因为>和>=是两个不同的运算符,需要区分。
4.2 语法分析器(Parser)- "检查语法车间"
4.2.1 它在做什么?
检查Token序列是否符合语法规则,并理解代码的结构。
就像检查"我 吃 饭"符合"主语+谓语+宾语"的语法规则。
4.2.2 为什么要这样做?
确保代码结构正确,为生成中间代码做准备。
4.2.3 核心语法规则
我们使用这些规则来分析表达式:
E → E + T | E - T | T (表达式由加减运算组成)
T → T * F | T / F | F (项由乘除运算组成)
F → id | num | (E) (因子是变量、数字或括号表达式)
这是什么意思?
E(Expression):表达式T(Term):项F(Factor):因子→:可以推导为|:或者
实际例子分析 :a + b * c
- 先看成
E + T T是b * c,即T * F- 最终结构:
a + (b * c)
4.2.4 关键方法详解
1. E() - 表达式分析
cpp
string E() {
string left = T(); // 先分析左边的项
// 如果后面有+或-,继续分析
while (getCurrentToken().type == 22 || getCurrentToken().type == 23) {
string op = getCurrentToken().value; // 获取运算符
match(getCurrentToken().type); // 消耗掉这个运算符
string right = T(); // 分析右边的项
left = ig->genArithmetic(left, op, right); // 生成中间代码
}
return left;
}
工作流程:
- 先调用
T()分析第一个项 - 检查后面有没有
+或- - 如果有,继续分析后面的项
- 生成对应的中间代码
2. parseCondition() - 分析if语句
cpp
void parseCondition() {
match(6); // 匹配if关键字
match(26); // 匹配左括号(
// 分析条件表达式
string left = E();
string op = getCurrentToken().value;
match(getCurrentToken().type); // 匹配比较运算符
string right = E();
match(27); // 匹配右括号)
// 生成标签(像书签一样,用于跳转)
string falseLabel = ig->newLabel(); // else部分的入口
string endLabel = ig->newLabel(); // if语句结束
// 生成条件跳转:如果条件不成立,跳到else部分
ig->genConditionJump(left, op, right, falseLabel);
// 分析if成立时要执行的代码块
match(28); // 匹配{
parseStatementBlock();
match(29); // 匹配}
// 跳过else部分,直接到结束
ig->genUnconditionalJump(endLabel);
// 标记else部分开始
ig->genLabel(falseLabel);
// 如果有else,分析else部分
if (getCurrentToken().type == 7) {
match(7); // 匹配else
match(28); // 匹配{
parseStatementBlock();
match(29); // 匹配}
}
// 标记if语句结束
ig->genLabel(endLabel);
}
if语句的翻译思路:
如果条件不成立 → 跳转到else标签
执行if代码块
跳转到结束标签(跳过else部分)
else标签:
执行else代码块
结束标签:
继续后面的代码
4.3 中间代码生成器(IntermediateGenerator)- "生成通用指令车间"
4.3.1 它在做什么?
生成一种介于源代码和机器代码之间的通用指令格式------四元式。
4.3.2 为什么要用中间代码?
- 便于优化:在中间代码层面做优化比较容易
- 便于移植:换目标机器时,只需要改后半部分
- 简化设计:把复杂问题分解
4.3.3 四元式是什么?
四元式格式:(操作数1, 运算符, 操作数2, 结果)
例子 :c = a + b * 2 会被翻译成:
(2, *, b, T1) // T1 = 2 * b
(a, +, T1, T2) // T2 = a + T1
(T2, =, , c) // c = T2
4.3.4 核心代码详解
1. 四元式结构
cpp
struct FourCom {
string arg1; // 第一个参数
string opera; // 操作符
string arg2; // 第二个参数
string result; // 结果
};
2. 生成算术运算
cpp
string genArithmetic(string arg1, string op, string arg2) {
string temp = newTemp(); // 生成临时变量名,比如T1、T2
addFourCom(arg1, op, arg2, temp); // 添加四元式
return temp; // 返回临时变量名,供后面使用
}
3. 生成条件跳转
cpp
void genConditionJump(string arg1, string op, string arg2, string label) {
addFourCom(arg1, op, arg2, label);
}
// 例子:(a, >, b, L1) 意思是:如果a>b,跳转到标签L1
4. 管理临时变量
cpp
string newTemp() {
return "T" + to_string(tempCounter++);
}
// 第一次调用返回"T0",第二次返回"T1",以此类推
4.4 中间代码优化器(Optimizer)- "优化车间"
4.4.1 它在做什么?
对中间代码进行优化,让最终的程序运行更快、占用空间更小。
4.4.2 为什么要优化?
有些计算在编译时就能确定结果,不需要等到运行时。
4.4.3 常量折叠优化
优化前:
(5, *, 2, T1) // T1 = 5 * 2
(10, +, T1, T2) // T2 = 10 + T1
优化后:
(20, =, , T2) // T2 = 20 (直接计算出结果)
4.4.4 核心代码详解
cpp
vector<FourCom> constantFolding(const vector<FourCom>& fourComs) {
vector<FourCom> optimized;
for (const auto& com : fourComs) {
// 检查是否是算术运算,并且两个操作数都是数字
if ((com.opera == "+" || com.opera == "-" ||
com.opera == "*" || com.opera == "/") &&
isNumber(com.arg1) && isNumber(com.arg2)) {
// 在编译时计算结果
double result = calculate(com.arg1, com.opera, com.arg2);
// 替换为简单的赋值
optimized.push_back(FourCom(to_string(result), "=", "", com.result));
} else {
optimized.push_back(com); // 无法优化,直接保留
}
}
return optimized;
}
4.5 目标代码生成器(AssemblyGenerator)- "生成最终产品车间"
4.5.1 它在做什么?
把四元式翻译成x86汇编代码,这是计算机能直接理解的语言。
4.5.2 为什么要生成汇编?
汇编是机器指令的文本表示,再通过汇编器就能变成真正的机器代码。
4.5.3 核心代码详解
1. 处理赋值语句
cpp
if (com.opera == "=") {
if (isalpha(com.arg1[0])) {
// 如果是变量:MOV EAX, [变量名]
assembly.push_back("MOV EAX, [" + com.arg1 + "]");
} else {
// 如果是数字:MOV EAX, 数字
assembly.push_back("MOV EAX, " + com.arg1);
}
assembly.push_back("MOV [" + com.result + "], EAX");
}
2. 处理加法
cpp
else if (com.opera == "+") {
assembly.push_back("MOV EAX, [" + com.arg1 + "]"); // 把第一个数放到EAX
assembly.push_back("ADD EAX, [" + com.arg2 + "]"); // 加上第二个数
assembly.push_back("MOV [" + com.result + "], EAX"); // 结果存回去
}
3. 处理条件跳转
cpp
else if (com.opera == ">") {
assembly.push_back("MOV EAX, [" + com.arg1 + "]"); // 加载第一个数
assembly.push_back("CMP EAX, [" + com.arg2 + "]"); // 比较两个数
assembly.push_back("JG " + com.result); // 如果大于就跳转
}
五、完整编译流程示例
让我们跟踪一个简单例子的完整编译过程:
源代码:
c
main() {
a = 10 + 5 * 2;
}
5.1 词法分析结果:
(1, "main") (26, "(") (27, ")") (28, "{")
(10, "a") (21, "=") (20, "10") (22, "+")
(20, "5") (24, "*") (20, "2") (31, ";")
(29, "}") (0, "#")
5.2 语法分析过程:
- 找到main函数
- 分析
a = 10 + 5 * 2;- 先分析
5 * 2(乘法优先级高) - 再分析
10 + 结果
- 先分析
5.3 中间代码生成:
(5, *, 2, T0) // T0 = 5 * 2
(10, +, T0, T1) // T1 = 10 + T0
(T1, =, , a) // a = T1
5.4 代码优化:
(20, =, , a) // 直接计算出10+5*2=20
5.5 目标代码生成:
asm
.data
a dd ?
.code
main proc
MOV EAX, 20 ; 把20放到EAX寄存器
MOV [a], EAX ; 把EAX的值存到变量a
main endp
end main
六、实现一个编译器
6.1 实现步骤
第一步:实现词法分析器
- 先识别单个字符
- 再识别关键字和变量名
- 最后识别数字和运算符
- 添加行号跟踪
第二步:实现简单的语法分析
- 先支持赋值语句:
变量 = 表达式 - 再支持算术表达式
- 最后支持控制结构
第三步:生成中间代码
- 设计四元式结构
- 实现临时变量管理
- 生成基本的运算四元式
第四步:生成目标代码
- 选择一种目标语言(汇编、C等)
- 实现四元式到目标代码的翻译
- 测试简单程序
6.2 调试技巧
- 分阶段测试:确保每个阶段都正确后再进入下一阶段
- 打印中间结果:在关键步骤输出中间状态
- 使用简单测试用例 :从
a = 1;这样的简单代码开始
6.3 扩展想法
完成基础版本后,可以添加:
- 更多的数据类型(浮点数、字符)
- 函数调用
- 数组支持
- 更复杂的优化
- 错误提示和恢复
七、总结
这个迷你编译器虽然简单,但包含了现代编译器的所有核心思想:
- 模块化设计:每个阶段职责明确
- 逐步翻译:从高级语言到低级语言的渐进转换
- 中间表示:使用四元式作为通用中间格式
- 优化思想:在编译时完成尽可能多的工作