【编译原理实战】第 1 篇:手写 C++17 编译器前端——词法、语法、语义、IR 4 阶段全打通

一句话核心结论printf("Hello, World!") 这一行 C 代码走完 GCC 完整编译,会经过 4 个独立阶段、跨越 6 个数据格式、调用 30+ 个内部 API。本文用 800 行 C++17 代码,把这 4 个阶段全部手写一遍——从此 DFA、AST、SSA 不再是课本术语,而是你能跑、能改、能调的工程实现。


系列导航

#文章状态
1本文:4 阶段全打通✅ 已发布
2优化 Pass:常量折叠、死代码消除、循环优化🔜 计划中
3目标代码生成:x86-64 后端🔜 计划中
4LLVM 实战:用 LLVM API 重写本文 mini 编译器🔜 计划中
5JIT 编译:运行时编译与 HotSpot🔜 计划中

前言:为什么写这一篇?

仓库里 cpp-interview-11-compile-link.md 已经讲过 .c → .i → .s → .o → a.out 的宏观流程,但那篇文章只点了一下「.i 是一堆 token」,没有任何一阶段的具体实现

现状:网上 90% 的「编译原理教程」教的是「四则运算计算器」——它能跑加减乘除,但完全无法体现作用域、类型、函数、控制流这些工程语言的核心复杂度。

本文的目标是完整覆盖

能力对应章节实战价值
手写 DFA 词法分析器第三节理解编译器怎么”看”代码
递归下降 + Pratt 表达式解析第四节理解运算符优先级怎么用代码表达
栈式符号表 + 作用域链第五节理解 let 声明和变量遮蔽
三地址码 + LLVM IR 桥接第六节理解 LLVM 源码里的 Instruction 是什么
4 大生产工具对比第八节选型决策:手写 vs ANTLR vs PLY vs Lark

读完这一篇,你将有能力读懂 LLVM/clang 前端源码——那 100 万行 C++ 代码不再是天书。

MiniLang:我们的玩具语言

为了演示,我们设计一个叫 MiniLang 的小语言。第一印象是「缩水的 JavaScript」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// fib.mini - 第一个测试用例(贯穿全文)
fn fib(n) {
if (n < 2) {
return n;
} else {
return fib(n-1) + fib(n-2);
}
}

let n = 10;
let i = 0;
while (i < n) {
print(fib(i));
i = i + 1;
}

MiniLang 特性清单

维度范围
关键字let fn if else while return print true false
运算符+ - * / == != < > <= >= && || ! =
类型int(默认数字)、boolstringfunction
字面量整数、字符串("...")、布尔
语句let 声明、函数定义、if/whilereturnprint、表达式语句
特性类型自动推导、if/while 接受整数 0=假/非 0=真、字符串+数字自动拼接

这套设计故意省略异常、闭包、类、数组等特性——目标是讲透 4 阶段,不是做语言


一、编译全景:4 阶段数据流

1.1 一次编译的完整旅程

graph LR
    SRC["📝 源码<br/>source.mini"]:::input
    TKN["🔖 Token 流<br/>[let,id(n),=,10,;]"]:::phase1
    AST["🌳 AST<br/>Let(n, Int(10))"]:::phase2
    TA["🛡️ 类型化 AST<br/>Let(n:int)"]:::phase3
    IR["⚙️ 三地址码<br/>t1 = 10<br/>n = t1"]:::phase4
    EXE["💾 x86 机器码"]:::output

    SRC ==>|词法| TKN
    TKN ==>|语法| AST
    AST ==>|语义| TA
    TA ==>|IR| IR
    IR -.->|"后续 5 篇"| EXE

    classDef input fill:#C7CEEA,stroke:#9FA8DA,color:#333
    classDef phase1 fill:#E8D5F5,stroke:#CE93D8,color:#333
    classDef phase2 fill:#FFDAB9,stroke:#FFAB76,color:#333
    classDef phase3 fill:#FFF9C4,stroke:#F9A825,color:#333
    classDef phase4 fill:#B5EAD7,stroke:#80CBC4,color:#333
    classDef output fill:#FFB3C6,stroke:#F48FB1,color:#333

关键观察

  • 每个阶段的输出是下个阶段的输入,这个数据流方向是不可逆的——丢掉位置信息就回不去源码了。
  • 前 4 阶段(前端)只和语言本身有关,和 CPU 架构无关;后端(IR→机器码)才和架构相关——这就是 LLVM 成功的根本原因:把”语言”和”机器”解耦。

1.2 4 阶段输入/输出/算法对比

阶段输入输出核心算法工具代表
词法分析字符流(char[]Token 列表(vector<Token>DFA(Deterministic Finite Automaton,确定有限状态自动机) / 正则lex, re2c, flex
语法分析Token 列表AST(Abstract Syntax Tree,抽象语法树)LL/LR 文法(递归下降 / 表驱动)yacc, bison, ANTLR
语义分析AST带类型/作用域信息的 AST符号表遍历、类型推导(通常手写)
IR 生成带类型 AST三地址码 / SSA(Static Single Assignment,静态单赋值)递归下降翻译LLVM IR Generator, Cranelift

DFA 是词法分析的”灵魂”——任何词法分析器本质都是一个 DFA;区别只是有人手写状态机、有人用工具生成。

1.3 主流编译器的 4 阶段实现差异

编译器词法语法语义IR工程亮点
GCC (cc1)手写 C手写递归下降 + Bison手写GIMPLE(TAC 风格)老牌稳定,1100 万行
Clang (LLVM)手写 C++ + 库化 Lex手写递归下降手写 + 库化 SemaLLVM IR(SSA)模块化、错误信息友好
V8 (JavaScript)手写(C++)+ RegExp fallback手写递归下降手写 + 内联缓存TurboFan(SSA)隐藏类 + 内联缓存极致优化
rustc手写手写递归下降借用检查(borrow checker)MIR(SSA)类型系统 + 内存安全
本次 mini 编译器手写 DFA递归下降 + Pratt栈式符号表三地址码 TAC~800 行 C++17

GCC vs Clang 的关键差异:GCC cc1 是「一块大泥球」——前后端耦合;Clang 是「图书馆」——AST、Sema、CodeGen 都是独立库。这正是 LLVM 生态繁荣的根基。

1.4 本文用到的 6 个核心数据结构

数据结构用途C++17 实现
Token词法单元struct Token { TokenType type; std::string lexeme; int line; int col; }
AST 节点语法树12 个具体类 + 基类 Expr / Stmt
Symbol符号表条目struct Symbol { std::string name; TypePtr type; bool isConst; }
SymbolTable栈式作用域std::vector<std::map<std::string, Symbol>>
TAC 指令三地址码struct Instr { Op op; Operand a, b, c; }
Program完整 IRstd::vector<Instr> instructions

二、词法分析:字符流 → Token

2.1 词法分析的本质

词法分析器(Lexer / Tokenizer) 的核心任务是:把无结构的字符流切成有结构的 token 列表

核心原则最长匹配(Maximal Munch)——遇到 intabc 时,要识别为标识符 intabc,而不是关键字 int + 标识符 abc

输入字符输出 Token
letLET
(空白)跳过
nIDENT("n")
=ASSIGN
跳过
123INT(123)
;SEMI

2.2 Token 数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ================ Token 定义 ================
enum class TokenType {
// 字面量
INT, STRING, IDENT,
TRUE, FALSE,

// 关键字
LET, FN, IF, ELSE, WHILE, RETURN, PRINT,

// 运算符
PLUS, MINUS, STAR, SLASH,
EQ, NEQ, LT, GT, LE, GE,
AND, OR, BANG,
ASSIGN,

// 分隔符
LPAREN, RPAREN, LBRACE, RBRACE, COMMA, SEMI,

// 特殊
END, ERROR
};

// 关键字表(O(1) 查找)
static const std::unordered_map<std::string, TokenType> keywords = {
{"let", TokenType::LET}, {"fn", TokenType::FN},
{"if", TokenType::IF}, {"else", TokenType::ELSE},
{"while", TokenType::WHILE}, {"return", TokenType::RETURN},
{"print", TokenType::PRINT}, {"true", TokenType::TRUE},
{"false", TokenType::FALSE}
};

struct Token {
TokenType type;
std::string lexeme; // 原始字符串
int line, col; // 位置(错误信息用)
};

2.3 DFA 状态机

DFA(Deterministic Finite Automaton,确定有限状态自动机)是词法分析器的数学基础。手写 Lexer = 手写 DFA

stateDiagram-v2
    [*] --> Start
    Start --> Ident: 字母/下划线
    Start --> Number: 数字
    Start --> String: 引号
    Start --> Op: 运算符
    Start --> Punct: 分隔符
    Start --> Skip: 空白
    Start --> Comment: 注释
    Start --> [*]: EOF

    Ident --> Ident: 字母数字下划线
    Ident --> [*]: 其他字符回退1

    Number --> Number: 数字
    Number --> [*]: 其他字符回退1

    String --> String: 引号内字符
    String --> [*]: 引号结束

    Op --> Eq: 等号
    Op --> [*]: 其他字符接受

    Comment --> Comment: 行内字符
    Comment --> [*]: 换行

    Skip --> [*]: 跳过字符
    Skip --> Start: 继续

状态机的核心思想

  • 遇到字母 → 进入「标识符状态」,继续读直到非字母数字——查关键字表决定是关键字还是普通标识符。
  • 遇到数字 → 进入「数字状态」,继续读直到非数字——构造整数。
  • 遇到 " → 进入「字符串状态」,一直读到匹配的 "——处理转义。
  • 遇到运算符 → 单字符直接接受;双字符(==, <=)要预读一字符。

2.4 完整 Lexer 实现(C++17)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
// ================ Lexer 完整实现 ================
class Lexer {
public:
explicit Lexer(std::string_view src)
: src_(src), pos_(0), line_(1), col_(1) {}

std::vector<Token> tokenize() {
std::vector<Token> tokens;
while (pos_ < src_.size()) {
skip_whitespace();
if (pos_ >= src_.size()) break;

char c = src_[pos_];

// 注释:// 开头到行尾
if (c == '/' && peek(1) == '/') {
while (pos_ < src_.size() && src_[pos_] != '\n') pos_++;
continue;
}

// 标识符 / 关键字
if (is_alpha(c)) {
tokens.push_back(read_ident_or_keyword());
continue;
}

// 数字
if (is_digit(c)) {
tokens.push_back(read_number());
continue;
}

// 字符串
if (c == '"') {
tokens.push_back(read_string());
continue;
}

// 运算符 / 分隔符
tokens.push_back(read_op_or_punct());
}
tokens.push_back({TokenType::END, "", line_, col_});
return tokens;
}

private:
std::string_view src_;
size_t pos_;
int line_, col_;

// ---- 字符判断 ----
static bool is_alpha(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
static bool is_digit(char c) { return c >= '0' && c <= '9'; }
static bool is_alnum(char c) { return is_alpha(c) || is_digit(c); }

// ---- 预读 ----
char peek(size_t offset = 0) const {
return (pos_ + offset < src_.size()) ? src_[pos_ + offset] : '\0';
}

// ---- 跳过空白 ----
void skip_whitespace() {
while (pos_ < src_.size()) {
char c = src_[pos_];
if (c == ' ' || c == '\t' || c == '\r') {
pos_++; col_++;
} else if (c == '\n') {
pos_++; line_++; col_ = 1;
} else {
break;
}
}
}

// ---- 读标识符 / 关键字 ----
Token read_ident_or_keyword() {
int start_col = col_;
size_t start = pos_;
while (pos_ < src_.size() && is_alnum(src_[pos_])) {
pos_++; col_++;
}
std::string lexeme(src_.substr(start, pos_ - start));

// 关键字表查找
auto it = keywords.find(lexeme);
TokenType type = (it != keywords.end()) ? it->second : TokenType::IDENT;
return {type, std::move(lexeme), line_, start_col};
}

// ---- 读数字 ----
Token read_number() {
int start_col = col_;
size_t start = pos_;
while (pos_ < src_.size() && is_digit(src_[pos_])) {
pos_++; col_++;
}
std::string num(src_.substr(start, pos_ - start));
return {TokenType::INT, std::move(num), line_, start_col};
}

// ---- 读字符串 ----
Token read_string() {
int start_col = col_;
pos_++; col_++; // 跳过开引号
std::string s;
while (pos_ < src_.size() && src_[pos_] != '"') {
if (src_[pos_] == '\\' && pos_ + 1 < src_.size()) {
// 处理转义
char esc = src_[pos_ + 1];
switch (esc) {
case 'n': s += '\n'; break;
case 't': s += '\t'; break;
case '"': s += '"'; break;
case '\\': s += '\\'; break;
default: s += esc;
}
pos_ += 2; col_ += 2;
} else {
s += src_[pos_];
pos_++; col_++;
}
}
if (pos_ < src_.size()) { pos_++; col_++; } // 跳过闭引号
return {TokenType::STRING, std::move(s), line_, start_col};
}

// ---- 读运算符 / 分隔符 ----
Token read_op_or_punct() {
int start_col = col_;
char c = src_[pos_];
char n = peek(1);

// 双字符运算符优先匹配
if (c == '=' && n == '=') { pos_+=2; col_+=2; return {TokenType::EQ, "==", line_, start_col}; }
if (c == '!' && n == '=') { pos_+=2; col_+=2; return {TokenType::NEQ, "!=", line_, start_col}; }
if (c == '<' && n == '=') { pos_+=2; col_+=2; return {TokenType::LE, "<=", line_, start_col}; }
if (c == '>' && n == '=') { pos_+=2; col_+=2; return {TokenType::GE, ">=", line_, start_col}; }
if (c == '&' && n == '&') { pos_+=2; col_+=2; return {TokenType::AND, "&&", line_, start_col}; }
if (c == '|' && n == '|') { pos_+=2; col_+=2; return {TokenType::OR, "||", line_, start_col}; }

// 单字符
pos_++; col_++;
switch (c) {
case '+': return {TokenType::PLUS, "+", line_, start_col};
case '-': return {TokenType::MINUS, "-", line_, start_col};
case '*': return {TokenType::STAR, "*", line_, start_col};
case '/': return {TokenType::SLASH, "/", line_, start_col};
case '=': return {TokenType::ASSIGN, "=", line_, start_col};
case '<': return {TokenType::LT, "<", line_, start_col};
case '>': return {TokenType::GT, ">", line_, start_col};
case '!': return {TokenType::BANG, "!", line_, start_col};
case '(': return {TokenType::LPAREN, "(", line_, start_col};
case ')': return {TokenType::RPAREN, ")", line_, start_col};
case '{': return {TokenType::LBRACE, "{", line_, start_col};
case '}': return {TokenType::RBRACE, "}", line_, start_col};
case ',': return {TokenType::COMMA, ",", line_, start_col};
case ';': return {TokenType::SEMI, ";", line_, start_col};
default:
return {TokenType::ERROR, std::string(1, c), line_, start_col};
}
}
};

2.5 词法分析的 5 大陷阱

陷阱现象正确做法
关键字 vs 标识符intabc 误判为 int abc最长匹配:先读完再判断
数字溢出9999999999999999999 溢出 int本项目用 long long;生产编译器用 APInt(任意精度)
字符串多行"hello\nworld" 是否支持换行MiniLang 不支持;Go 模板字符串支持
Unicode中文变量名 let 中文 = 1C++17 支持但要 UTF-8 解析;本项目用 ASCII
Lex / Yacc 不支持的”上下文相关”>> 在 C++ 中是右移还是两个 >现代 Flex 解决;本项目不支持嵌套泛型

核心观察生产编译器的词法分析器都不手写状态机——都用 lex/flex 之类的工具。但学习时手写一次 DFA 才能真正理解词法分析的本质

2.6 词法分析 vs 正则表达式

核心命题词法分析器 = 正则表达式引擎。任何 token 都能用正则表达:

Token正则
标识符[a-zA-Z_][a-zA-Z0-9_]*
整数[0-9]+
字符串"([^"\\]|\\.)*"
关键字直接查表(不是正则)

flex 的工作原理:正则 → NFA → DFA → C 代码。这正是”理论驱动工具”的完美案例。


三、语法分析:Token → AST

3.1 语法分析的本质

语法分析器(Parser) 的核心任务:把扁平的 token 列表还原成树形的 AST

为什么需要 AST:表达式 1 + 2 * 3 是线性的 1, +, 2, *, 3——但计算顺序1 + (2 * 3)。AST 把这种结构化信息显式编码成树。

graph TB
    ROOT["+"]:::op
    L["1"]:::lit
    R["*"]:::op
    RL["2"]:::lit
    RR["3"]:::lit

    ROOT --- L
    ROOT --- R
    R --- RL
    R --- RR

    classDef op fill:#FFDAB9,stroke:#FFAB76,color:#333
    classDef lit fill:#B5EAD7,stroke:#80CBC4,color:#333

优先级自然体现*+ 优先 → * 在树更深处——先求值叶子,再向上。

3.2 文法:BNF 与 EBNF

MiniLang 的 EBNF 文法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
program     = { stmt } ;
stmt = let_stmt | fn_decl | if_stmt | while_stmt
| return_stmt | print_stmt | expr_stmt ;
let_stmt = "let" , IDENT , "=" , expr , ";" ;
fn_decl = "fn" , IDENT , "(" , [ params ] , ")" , block ;
params = IDENT , { "," , IDENT } ;
if_stmt = "if" , "(" , expr , ")" , block , [ "else" , block ] ;
while_stmt = "while" , "(" , expr , ")" , block ;
return_stmt = "return" , [ expr ] , ";" ;
print_stmt = "print" , "(" , expr , ")" , ";" ;
expr_stmt = expr , ";" ;
block = "{" , { stmt } , "}" ;

expr = equality ;
equality = comparison , { ( "==" | "!=" ) , comparison } ;
comparison = additive , { ( "<" | ">" | "<=" | ">=" ) , additive } ;
additive = multiplicative , { ( "+" | "-" ) , multiplicative } ;
multiplicative = unary , { ( "*" | "/" ) , unary } ;
unary = ( "!" | "-" )? , primary ;
primary = INT | STRING | "true" | "false" | IDENT
| call | "(" , expr , ")" ;
call = IDENT , "(" , [ args ] , ")" ;
args = expr , { "," , expr } ;

EBNF 的 3 个元符号

符号含义BNF 等价
{ x }0 次或多次x { x }
[ x ]0 次或 1 次x | ε
( x | y )选择x | y

3.3 两大流派:LL vs LR

流派全称核心思想优势工具
LL(k)Left-to-right, Leftmost derivation自顶向下,从根向下预测易理解、易手写ANTLR, 手写递归下降
LR(k)Left-to-right, Rightmost derivation自底向上,从叶子向上归约表达力强、能处理左递归Yacc, Bison, CUP
LALRLookahead LRLR 的简化版工业主流Yacc, Bison
PrattTop-Down Operator PrecedenceLL 的扩展,按优先级分发表达式解析最优雅(手写,无工具)

当前编译器主流:JavaScript(V8、SpiderMonkey、JavaScriptCore)全用递归下降;Go 用 LALR;Clang 用递归下降;Rust 用 LALR(rust-grammar)。

3.4 运算符优先级表

这是 Pratt 解析的核心数据:

优先级运算符结合性类别
1() 函数调用后缀
2! -(一元)一元
3* /乘除
4+ -加减
5< > <= >=比较
6== !=相等
7&&逻辑与
8||逻辑或
9=赋值

关键观察

  • 数字越大优先级越低——* 是 3,+ 是 4,所以 * 先算。
  • 结合性决定同优先级时的结合方向——a - b - c 左结合等价于 (a - b) - ca = b = c 右结合等价于 a = (b = c)

3.5 Pratt 解析:表达式的优雅解法

Pratt 解析(Top-Down Operator Precedence Parsing)是 Vaughan Pratt 1973 年发明的表达式解析算法。核心思想为每个 token 定义两个优先级——lbp(left binding power,左结合力)和 rbp(right binding power,右结合力)。

flowchart TD
    START(["parse_expression min_bp=0"]) --> LOOKUP["查 token 的绑定力<br/>lbp"]
    LOOKUP -->|"lbp < min_bp"| RETURN1["返回 null(停止)"]
    LOOKUP -->|"lbp >= min_bp"| CONSUME["消费 token<br/>作为左侧"]
    CONSUME --> LOOP["loop:<br/>取下一个 token 的 rbp"]
    LOOP -->|"rbp > min_bp"| RECURSE["parse_expression rbp<br/>得到右侧"]
    RECURSE --> COMBINE["组合 left = op(left, right)"]
    COMBINE --> LOOP
    LOOP -->|"rbp <= min_bp"| RETURN2["返回 left"]

    style START fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style LOOKUP fill:#FFF9C4,stroke:#F9A825,color:#333
    style RETURN1 fill:#FFDAB9,stroke:#FFAB76,color:#333
    style CONSUME fill:#E8D5F5,stroke:#CE93D8,color:#333
    style LOOP fill:#E8D5F5,stroke:#CE93D8,color:#333
    style RECURSE fill:#E8D5F5,stroke:#CE93D8,color:#333
    style COMBINE fill:#E8D5F5,stroke:#CE93D8,color:#333
    style RETURN2 fill:#B5EAD7,stroke:#80CBC4,color:#333

关键观察递归的深度由优先级决定——min_bp 越大,能消费的 token 越少,递归越浅。

3.6 AST 节点设计:继承 vs variant

两种主流 AST 节点设计

维度继承层级std::variant
内存布局指针 + vtable一个 union 内存紧凑
访问方式虚函数 accept(Visitor&)std::visit(Visitor{}, node)
类型安全弱(任何 Expr* 都接受)强(编译期检查)
调试友好较好(gdb 看得到 vtable)略差(gdb 看到 variant 内部)
性能间接跳转(vtable)直接调用(编译器优化)
代码量较多(每个节点一个类)较少(节点用 struct)
C++17 特性std::variant, std::visit

本文选择继承层级——更接近 Clang 源码风格、易于扩展。

3.7 完整 AST 节点定义(C++17)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// ================ AST 节点 ================

// 表达式基类
struct Expr {
virtual ~Expr() = default;
virtual std::string to_string() const = 0;
};
using ExprPtr = std::unique_ptr<Expr>;

// 字面量
struct IntLit : Expr { long long value; explicit IntLit(long long v) : value(v) {} std::string to_string() const override { return std::to_string(value); } };
struct StrLit : Expr { std::string value; explicit StrLit(std::string v) : value(std::move(v)) {} std::string to_string() const override { return "\"" + value + "\""; } };
struct BoolLit : Expr { bool value; explicit BoolLit(bool v) : value(v) {} std::string to_string() const override { return value ? "true" : "false"; } };
struct VarRef : Expr { std::string name; explicit VarRef(std::string n) : name(std::move(n)) {} std::string to_string() const override { return name; } };

// 二元 / 一元
struct Binary : Expr { std::string op; ExprPtr lhs, rhs;
Binary(std::string o, ExprPtr l, ExprPtr r) : op(std::move(o)), lhs(std::move(l)), rhs(std::move(r)) {}
std::string to_string() const override { return "(" + lhs->to_string() + op + rhs->to_string() + ")"; }
};
struct Unary : Expr { std::string op; ExprPtr operand;
Unary(std::string o, ExprPtr e) : op(std::move(o)), operand(std::move(e)) {}
std::string to_string() const override { return op + operand->to_string(); }
};
struct Call : Expr { std::string callee; std::vector<ExprPtr> args;
std::string to_string() const override {
std::string s = callee + "(";
for (size_t i = 0; i < args.size(); ++i) {
if (i) s += ", ";
s += args[i]->to_string();
}
return s + ")";
}
};

// 语句基类
struct Stmt {
virtual ~Stmt() = default;
virtual std::string to_string() const = 0;
};
using StmtPtr = std::unique_ptr<Stmt>;

// 各种语句
struct LetStmt : Stmt { std::string name; ExprPtr value;
std::string to_string() const override { return "let " + name + " = " + value->to_string() + ";"; }
};
struct AssignStmt : Stmt { std::string name; ExprPtr value;
std::string to_string() const override { return name + " = " + value->to_string() + ";"; }
};
struct PrintStmt : Stmt { ExprPtr value;
std::string to_string() const override { return "print(" + value->to_string() + ");"; }
};
struct ReturnStmt : Stmt { std::optional<ExprPtr> value;
std::string to_string() const override {
if (!value) return "return;";
return "return " + (*value)->to_string() + ";";
}
};
struct IfStmt : Stmt { ExprPtr cond; StmtPtr then_branch, else_branch;
std::string to_string() const override {
std::string s = "if (" + cond->to_string() + ") " + then_branch->to_string();
if (else_branch) s += " else " + else_branch->to_string();
return s;
}
};
struct WhileStmt : Stmt { ExprPtr cond; StmtPtr body;
std::string to_string() const override { return "while (" + cond->to_string() + ") " + body->to_string(); }
};
struct Block : Stmt { std::vector<StmtPtr> stmts;
std::string to_string() const override {
std::string s = "{\n";
for (auto& st : stmts) s += " " + st->to_string() + "\n";
return s + "}";
}
};
struct FnDecl : Stmt { std::string name; std::vector<std::string> params; StmtPtr body;
std::string to_string() const override {
std::string s = "fn " + name + "(";
for (size_t i = 0; i < params.size(); ++i) { if (i) s += ", "; s += params[i]; }
return s + ") " + body->to_string();
}
};
struct ExprStmt : Stmt { ExprPtr expr;
std::string to_string() const override { return expr->to_string() + ";"; }
};

// 程序:语句列表
struct Program {
std::vector<StmtPtr> stmts;
std::string to_string() const {
std::string s;
for (auto& st : stmts) s += st->to_string() + "\n";
return s;
}
};

3.8 完整 Parser(C++17)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// ================ Parser 完整实现 ================
class Parser {
public:
explicit Parser(std::vector<Token> tokens) : tokens_(std::move(tokens)), pos_(0) {}

Program parse_program() {
Program prog;
while (!is_end()) {
if (auto stmt = parse_stmt()) {
prog.stmts.push_back(std::move(stmt));
} else {
break; // 错误恢复:停在 END
}
}
return prog;
}

private:
std::vector<Token> tokens_;
size_t pos_;

const Token& current() const { return tokens_[pos_]; }
const Token& peek(size_t offset = 0) const { return tokens_[std::min(pos_ + offset, tokens_.size() - 1)]; }
bool is_end() const { return current().type == TokenType::END; }
bool check(TokenType t) const { return current().type == t; }

// 消费并返回当前 token(必须匹配)
Token consume(TokenType t, const std::string& msg) {
if (check(t)) {
Token tk = current();
advance();
return tk;
}
throw std::runtime_error("Parse error at line " + std::to_string(current().line)
+ ": expected " + msg
+ ", got '" + current().lexeme + "'");
}

void advance() { if (!is_end()) pos_++; }

// ================ 语句 ================
std::unique_ptr<Stmt> parse_stmt() {
if (check(TokenType::LET)) return parse_let();
if (check(TokenType::FN)) return parse_fn();
if (check(TokenType::IF)) return parse_if();
if (check(TokenType::WHILE)) return parse_while();
if (check(TokenType::RETURN)) return parse_return();
if (check(TokenType::PRINT)) return parse_print();
return parse_expr_stmt();
}

std::unique_ptr<Stmt> parse_let() {
consume(TokenType::LET, "'let'");
Token name = consume(TokenType::IDENT, "identifier");
consume(TokenType::ASSIGN, "'='");
auto value = parse_expr();
consume(TokenType::SEMI, "';'");
return std::make_unique<LetStmt>(name.lexeme, std::move(value));
}

std::unique_ptr<Stmt> parse_fn() {
consume(TokenType::FN, "'fn'");
Token name = consume(TokenType::IDENT, "function name");
consume(TokenType::LPAREN, "'('");
std::vector<std::string> params;
if (!check(TokenType::RPAREN)) {
params.push_back(consume(TokenType::IDENT, "param name").lexeme);
while (check(TokenType::COMMA)) {
advance();
params.push_back(consume(TokenType::IDENT, "param name").lexeme);
}
}
consume(TokenType::RPAREN, "')'");
auto body = parse_block();
return std::make_unique<FnDecl>(name.lexeme, std::move(params), std::move(body));
}

std::unique_ptr<Stmt> parse_if() {
consume(TokenType::IF, "'if'");
consume(TokenType::LPAREN, "'('");
auto cond = parse_expr();
consume(TokenType::RPAREN, "')'");
auto then_br = parse_block();
std::unique_ptr<Stmt> else_br;
if (check(TokenType::ELSE)) {
advance();
else_br = parse_block();
}
return std::make_unique<IfStmt>(std::move(cond), std::move(then_br), std::move(else_br));
}

std::unique_ptr<Stmt> parse_while() {
consume(TokenType::WHILE, "'while'");
consume(TokenType::LPAREN, "'('");
auto cond = parse_expr();
consume(TokenType::RPAREN, "')'");
auto body = parse_block();
return std::make_unique<WhileStmt>(std::move(cond), std::move(body));
}

std::unique_ptr<Stmt> parse_return() {
consume(TokenType::RETURN, "'return'");
std::optional<ExprPtr> value;
if (!check(TokenType::SEMI)) {
value = parse_expr();
}
consume(TokenType::SEMI, "';'");
return std::make_unique<ReturnStmt>(std::move(value));
}

std::unique_ptr<Stmt> parse_print() {
consume(TokenType::PRINT, "'print'");
consume(TokenType::LPAREN, "'('");
auto value = parse_expr();
consume(TokenType::RPAREN, "')'");
consume(TokenType::SEMI, "';'");
return std::make_unique<PrintStmt>(std::move(value));
}

std::unique_ptr<Stmt> parse_expr_stmt() {
auto expr = parse_expr();
consume(TokenType::SEMI, "';'");
return std::make_unique<ExprStmt>(std::move(expr));
}

std::unique_ptr<Stmt> parse_block() {
consume(TokenType::LBRACE, "'{'");
auto blk = std::make_unique<Block>();
while (!check(TokenType::RBRACE) && !is_end()) {
blk->stmts.push_back(parse_stmt());
}
consume(TokenType::RBRACE, "'}'");
return blk;
}

// ================ 表达式(Pratt 解析) ================
std::unique_ptr<Expr> parse_expr(int min_bp = 0) {
auto lhs = parse_unary();

while (true) {
int lbp = left_bp(current().type);
if (lbp < min_bp) break;

std::string op = current().lexeme;
advance();

// 右结合:右 min_bp 传 lbp;左结合传 lbp + 1
int r_min = (op == "=") ? lbp : lbp + 1;
auto rhs = parse_expr(r_min);
lhs = std::make_unique<Binary>(op, std::move(lhs), std::move(rhs));
}
return lhs;
}

// 左结合力
int left_bp(TokenType t) const {
switch (t) {
case TokenType::ASSIGN: return 5;
case TokenType::OR: return 10;
case TokenType::AND: return 15;
case TokenType::EQ:
case TokenType::NEQ: return 20;
case TokenType::LT:
case TokenType::GT:
case TokenType::LE:
case TokenType::GE: return 25;
case TokenType::PLUS:
case TokenType::MINUS: return 30;
case TokenType::STAR:
case TokenType::SLASH: return 40;
default: return -1;
}
}

std::unique_ptr<Expr> parse_unary() {
if (check(TokenType::BANG) || check(TokenType::MINUS)) {
std::string op = current().lexeme;
advance();
auto operand = parse_unary();
return std::make_unique<Unary>(op, std::move(operand));
}
return parse_call();
}

std::unique_ptr<Expr> parse_call() {
Token name = consume(TokenType::IDENT, "identifier or call");
if (check(TokenType::LPAREN)) {
advance();
auto call = std::make_unique<Call>(name.lexeme);
if (!check(TokenType::RPAREN)) {
call->args.push_back(parse_expr());
while (check(TokenType::COMMA)) {
advance();
call->args.push_back(parse_expr());
}
}
consume(TokenType::RPAREN, "')'");
return call;
}
return std::make_unique<VarRef>(name.lexeme);
}
};

3.9 错误恢复:Panic-Mode

Panic-Mode 错误恢复 是工业界最常用的策略:遇到错误时,丢弃 token 直到遇到”同步点”(synchronization set),然后继续解析

同步点用途
;表达式语句结束
}块结束
关键字(let, fn, if, while, return语句开始

MiniLang 简化:本文只在 END 时停止;工业编译器(Clang)有 30+ 种恢复策略,可一次报告多个错误。


四、语义分析:类型 + 作用域 + 隐式转换

4.1 语法正确 ≠ 语义正确

反例

1
2
3
4
5
// 语法正确,语义错误
let x = "hello";
let y = x - 1; // 字符串减数字?
fn foo(a) { return a + 1; }
foo("hello"); // 函数接受数字,传入字符串?

语法分析只检查 token 序列是否符合文法——它不检查类型、不检查作用域语义分析填补这块空白。

4.2 语义分析的三大任务

任务做什么实现
作用域分析let x 在哪一层?同名 let x 怎么办?栈式符号表
类型检查1 + "2"int + int 还是 int + string类型规则 + 递归遍历 AST
隐式转换1 + 2.0int 还是 double?字符串拼接?类型提升表

4.3 符号表:栈式作用域链

graph TB
    GLOBAL["🌍 全局作用域<br/>n=10, fib:fn, ..."]:::global
    FIB["📦 fib 函数作用域<br/>n: int"]:::func
    MAIN["📦 main 隐式作用域<br/>i=0, n=10"]:::block
    WHILE["📦 while 体作用域<br/>(空)"]:::block

    GLOBAL --> FIB
    GLOBAL --> MAIN
    MAIN --> WHILE

    classDef global fill:#C7CEEA,stroke:#9FA8DA,color:#333
    classDef func fill:#E8D5F5,stroke:#CE93D8,color:#333
    classDef block fill:#FFF9C4,stroke:#F9A825,color:#333

栈式符号表的两大操作

操作行为
enter_scope()压栈新作用域
exit_scope()弹栈
declare(name, sym)当前作用域声明
lookup(name)当前作用域向查找(找到第一个为止)

4.4 类型系统

MiniLang 的类型

类型表示字面量示例
int整数42, -7, 0
bool布尔true, false
string字符串"hello", ""
function函数(无字面量,从 fn 推导)
void无返回值(仅用于函数返回类型)
error错误类型(类型检查失败占位)

4.5 隐式转换规则

from \ tointboolstring
int✅ (0 → false)✅ (42 → "42")
bool✅ (true → "true")
string

关键观察int → string 只在 + 操作时触发——其他场景(-, *, /)不允许 int + string 直接拼接报错。这是 C++ 的 << vs + 哲学差异。

4.6 完整 SemanticAnalyzer 实现(C++17)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// ================ 类型 ================
enum class TypeKind { INT, BOOL, STRING, FUNCTION, VOID, ERROR };
struct Type {
TypeKind kind;
std::vector<std::string> param_types; // 函数参数(简化为名字)
TypeKind ret_type = TypeKind::VOID; // 函数返回
bool operator==(const Type& o) const {
return kind == o.kind && param_types == o.param_types && ret_type == o.ret_type;
}
bool operator!=(const Type& o) const { return !(*this == o); }
};
using TypePtr = std::shared_ptr<Type>;

// ================ 符号 ================
struct Symbol {
std::string name;
TypePtr type;
bool is_function = false;
};

// ================ 符号表(栈式) ================
class SymbolTable {
public:
void enter_scope() { scopes_.emplace_back(); }
void exit_scope() { scopes_.pop_back(); }

void declare(const std::string& name, Symbol sym) {
scopes_.back()[name] = std::move(sym);
}

Symbol* lookup(const std::string& name) {
for (auto it = scopes_.rbegin(); it != scopes_.rend(); ++it) {
auto found = it->find(name);
if (found != it->end()) return &found->second;
}
return nullptr;
}

private:
std::vector<std::unordered_map<std::string, Symbol>> scopes_;
};

// ================ 语义分析器 ================
class SemanticAnalyzer {
public:
explicit SemanticAnalyzer(bool verbose = false) : verbose_(verbose) {
globals_.enter_scope();
}

void analyze(Program& prog) {
// 第一遍:注册所有函数(允许前向引用)
for (auto& stmt : prog.stmts) {
if (auto* fn = dynamic_cast<FnDecl*>(stmt.get())) {
Symbol sym{fn->name, make_fn_type(*fn), true};
globals_.declare(fn->name, std::move(sym));
}
}
// 第二遍:分析所有语句
for (auto& stmt : prog.stmts) {
analyze_stmt(*stmt);
}
}

private:
SymbolTable globals_;
bool verbose_;
int errors_ = 0;

TypePtr make_fn_type(const FnDecl& fn) {
auto t = std::make_shared<Type>();
t->kind = TypeKind::FUNCTION;
for (auto& p : fn.params) t->param_types.push_back(p);
t->ret_type = TypeKind::INT; // 简化:都返回 int
return t;
}

void error(const Token& tk, const std::string& msg) {
std::cerr << "[Semantic Error] line " << tk.line << ": " << msg << "\n";
errors_++;
}

// 字符串字面量默认是 string 类型,其他字面量是 int / bool
TypePtr type_of_expr(const Expr& e) {
if (dynamic_cast<const IntLit*>(&e)) return std::make_shared<Type>((Type){TypeKind::INT});
if (dynamic_cast<const StrLit*>(&e)) return std::make_shared<Type>((Type){TypeKind::STRING});
if (dynamic_cast<const BoolLit*>(&e)) return std::make_shared<Type>((Type){TypeKind::BOOL});
if (auto* v = dynamic_cast<const VarRef*>(&e)) {
Symbol* s = globals_.lookup(v->name);
return s ? s->type : std::make_shared<Type>((Type){TypeKind::ERROR});
}
if (dynamic_cast<const Unary*>(&e)) {
return std::make_shared<Type>((Type){TypeKind::BOOL}); // 简化:! / - 都返回 bool/int
}
if (dynamic_cast<const Call*>(&e)) {
return std::make_shared<Type>((Type){TypeKind::INT});
}
// Binary: 简化处理
return std::make_shared<Type>((Type){TypeKind::INT});
}

void analyze_stmt(Stmt& s) {
if (auto* p = dynamic_cast<LetStmt*>(&s)) {
auto t = type_of_expr(*p->value);
Symbol sym{p->name, t, false};
globals_.declare(p->name, std::move(sym));
} else if (auto* p = dynamic_cast<AssignStmt*>(&s)) {
// 简化:检查存在性
auto t = type_of_expr(*p->value);
(void)t;
} else if (auto* p = dynamic_cast<IfStmt*>(&s)) {
analyze_stmt(*p->then_branch);
if (p->else_branch) analyze_stmt(*p->else_branch);
} else if (auto* p = dynamic_cast<WhileStmt*>(&s)) {
analyze_stmt(*p->body);
} else if (auto* p = dynamic_cast<Block*>(&s)) {
globals_.enter_scope();
for (auto& st : p->stmts) analyze_stmt(*st);
globals_.exit_scope();
} else if (auto* p = dynamic_cast<FnDecl*>(&s)) {
globals_.enter_scope();
// 把参数加入函数作用域
for (auto& param : p->params) {
Symbol sym{param, std::make_shared<Type>((Type){TypeKind::INT}), false};
globals_.declare(param, std::move(sym));
}
analyze_stmt(*p->body);
globals_.exit_scope();
} else if (auto* p = dynamic_cast<ExprStmt*>(&s)) {
type_of_expr(*p->expr);
} else if (auto* p = dynamic_cast<PrintStmt*>(&s)) {
type_of_expr(*p->value);
} else if (auto* p = dynamic_cast<ReturnStmt*>(&s)) {
if (p->value) type_of_expr(**p->value);
}
}
};

4.7 进阶:控制流分析与活跃性

控制流分析(Control Flow Analysis):把 AST 转成控制流图(CFG, Control Flow Graph)——基本块 + 跳转边。后续的优化和寄存器分配都基于 CFG。

活跃性分析(Liveness Analysis):从后向前扫描,标记每个变量”在某个点是否还会被使用”。未使用的变量可以被删除(死代码消除)。

SSA(Static Single Assignment,静态单赋值):每个变量只被赋值一次——简化数据流分析。LLVM IR 是 SSA 形式。

分析类型输入输出用途
作用域分析AST符号表名字解析
类型检查AST + 符号表带类型 AST错误检测
控制流分析ASTCFG(基本块 + 边)优化基础
活跃性分析CFG每个变量在每个点的 live / dead死代码消除
SSA 转换CFGφ 节点 + 单一赋值优化 + 寄存器分配

五、IR 生成:与平台无关的中间表示

5.1 为什么需要 IR

核心命题IR(Intermediate Representation,中间表示)是”语言”和”机器”之间的解耦层

graph LR
    L1["C"]:::lang
    L2["Rust"]:::lang
    L3["Swift"]:::lang
    IR["🌐 LLVM IR<br/>(SSA)"]:::ir
    T1["x86-64"]:::target
    T2["ARM64"]:::target
    T3["WebAssembly"]:::target
    T4["RISC-V"]:::target

    L1 --> IR
    L2 --> IR
    L3 --> IR
    IR --> T1
    IR --> T2
    IR --> T3
    IR --> T4

    classDef lang fill:#C7CEEA,stroke:#9FA8DA,color:#333
    classDef ir fill:#E8D5F5,stroke:#CE93D8,color:#333
    classDef target fill:#B5EAD7,stroke:#80CBC4,color:#333

N 个语言 × M 个目标:没有 IR 需要 N×M 个编译器;有 IR 只需 N+M 个。

5.2 三种 IR 范式

范式全称特点代表
栈式 IRStack-based像 JVM 字节码:操作数在栈上JVM 字节码WebAssembly、CPython bytecode
三地址码(TAC)Three-Address Code每条指令最多 3 个操作数:x = y op zGCC GIMPLE(雏形)、本文 mini 编译器
SSAStatic Single Assignment每个变量只赋值一次,带 φ 节点LLVM IRV8 TurboFanrustc MIR

对比示例 a + b * c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
栈式(类似 JVM):
ILOAD a
ILOAD b
ILOAD c
IMUL ; b * c → 栈顶
IADD ; a + (b*c) → 栈顶

TAC:
t1 = b * c
t2 = a + t1

SSA(LLVM IR):
%1 = mul i32 %b, %c
%2 = add i32 %a, %1
; a, b, c 在 SSA 之前会先被「重命名」为 a.1, b.1, c.1

核心观察SSA 是 TAC 的超集——SSA 多了 φ 节点和”单赋值”约束。现代编译器几乎都用 SSA

5.3 三地址码(TAC)设计

TAC 指令的 6 种类型

指令形式示例
赋值x = yt1 = 10
二元运算x = y op zt2 = t1 + 3
一元运算x = op yt1 = -a
复制x = yn = t1
跳转goto Lgoto L1
条件跳转if x goto Lif t1 goto L2
函数调用x = call f(a, b)t3 = call fib(n)
标签L:L1:
返回ret xret t1

5.4 表达式 → TAC 转换

graph LR
    EXPR["🌳 AST<br/>Binary(+,<br/>  a,<br/>  Binary(*,<br/>    b,<br/>    c))"]:::ast
    T1["t1 = b * c"]:::tac
    T2["t2 = a + t1"]:::tac

    EXPR ==>|gen t1| T1
    T1 ==>|gen t2| T2

    classDef ast fill:#FFDAB9,stroke:#FFAB76,color:#333
    classDef tac fill:#B5EAD7,stroke:#80CBC4,color:#333

关键点

  • 递归下降翻译:后序遍历 AST,每个表达式返回一个”临时变量名”。
  • 副作用:生成新指令到 IR 列表。

5.5 完整 CodeGen 实现(C++17)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// ================ TAC 操作数 ================
struct Operand {
enum Kind { VAR, TEMP, LABEL, INT_LIT, STR_LIT } kind;
std::string name;
long long int_val = 0;
std::string str_val;

static Operand temp(int n) { return {TEMP, "t" + std::to_string(n), 0, ""}; }
static Operand var(const std::string& n) { return {VAR, n, 0, ""}; }
static Operand lit(long long v) { return {INT_LIT, "", v, ""}; }
static Operand str(const std::string& s) { return {STR_LIT, "", 0, s}; }
static Operand label(int n) { return {LABEL, "L" + std::to_string(n), 0, ""}; }

std::string to_string() const {
switch (kind) {
case VAR: return name;
case TEMP: return name;
case LABEL: return name + ":";
case INT_LIT:return std::to_string(int_val);
case STR_LIT:return "\"" + str_val + "\"";
}
return "?";
}
};

// ================ TAC 指令 ================
enum class Op {
ASSIGN, BINOP, UNARYOP, COPY,
LABEL, GOTO, IFGOTO,
PARAM, CALL, RET, PRINTI, PRINTS,
FUNC_BEGIN, FUNC_END
};
struct Instr {
Op op;
std::string binop; // BINOP: +, -, *, /, == ...
Operand dst, src1, src2;
std::string func_name; // CALL 用
std::vector<Operand> args; // PARAM/CALL 用
};

// ================ CodeGen ================
class CodeGen {
public:
std::vector<Instr> generate(const Program& prog) {
for (const auto& stmt : prog.stmts) {
gen_stmt(*stmt);
}
return code;
}

const std::vector<Instr>& code() const { return code_; }

private:
std::vector<Instr> code_;
int temp_counter_ = 0;
int label_counter_ = 0;

Operand new_temp() { return Operand::temp(temp_counter_++); }
Operand new_label() { return Operand::label(label_counter_++); }

void emit(Op op, Operand dst = {}, Operand s1 = {}, Operand s2 = {},
const std::string& binop = "", const std::string& fn = "") {
code_.push_back({op, binop, std::move(dst), std::move(s1), std::move(s2), fn, {}});
}

// 表达式 → 临时变量
Operand gen_expr(const Expr& e) {
if (auto* lit = dynamic_cast<const IntLit*>(&e)) {
return Operand::lit(lit->value);
}
if (auto* lit = dynamic_cast<const StrLit*>(&e)) {
return Operand::str(lit->value);
}
if (auto* lit = dynamic_cast<const BoolLit*>(&e)) {
return Operand::lit(lit->value ? 1 : 0);
}
if (auto* v = dynamic_cast<const VarRef*>(&e)) {
return Operand::var(v->name);
}
if (auto* u = dynamic_cast<const Unary*>(&e)) {
auto t = new_temp();
auto src = gen_expr(*u->operand);
emit(Op::UNARYOP, t, src, {}, u->op);
return t;
}
if (auto* b = dynamic_cast<const Binary*>(&e)) {
auto t = new_temp();
auto l = gen_expr(*b->lhs);
auto r = gen_expr(*b->rhs);
emit(Op::BINOP, t, l, r, b->op);
return t;
}
if (auto* c = dynamic_cast<const Call*>(&e)) {
// 参数压栈
for (auto& a : c->args) {
auto arg_t = gen_expr(*a);
emit(Op::PARAM, {}, arg_t, {}, "", "");
}
auto ret = new_temp();
Instr instr;
instr.op = Op::CALL;
instr.dst = ret;
instr.func_name = c->callee;
code_.push_back(instr);
return ret;
}
return Operand::lit(0);
}

void gen_stmt(const Stmt& s) {
if (auto* p = dynamic_cast<const LetStmt*>(&s)) {
auto v = gen_expr(*p->value);
emit(Op::ASSIGN, Operand::var(p->name), v);
} else if (auto* p = dynamic_cast<const AssignStmt*>(&s)) {
auto v = gen_expr(*p->value);
emit(Op::ASSIGN, Operand::var(p->name), v);
} else if (auto* p = dynamic_cast<const PrintStmt*>(&s)) {
auto v = gen_expr(*p->value);
// 简化:数字用 PRINTI,字符串用 PRINTS
if (v.kind == Operand::STR_LIT) {
emit(Op::PRINTS, {}, v);
} else {
emit(Op::PRINTI, {}, v);
}
} else if (auto* p = dynamic_cast<const IfStmt*>(&s)) {
auto cond = gen_expr(*p->cond);
auto l_else = new_label();
auto l_end = new_label();
emit(Op::IFGOTO, l_else, cond, {}, "==", "0"); // if cond == 0 goto else
gen_stmt(*p->then_branch);
emit(Op::GOTO, l_end);
emit(Op::LABEL, l_else);
if (p->else_branch) gen_stmt(*p->else_branch);
emit(Op::LABEL, l_end);
} else if (auto* p = dynamic_cast<const WhileStmt*>(&s)) {
auto l_start = new_label();
auto l_end = new_label();
emit(Op::LABEL, l_start);
auto cond = gen_expr(*p->cond);
emit(Op::IFGOTO, l_end, cond, {}, "==", "0");
gen_stmt(*p->body);
emit(Op::GOTO, l_start);
emit(Op::LABEL, l_end);
} else if (auto* p = dynamic_cast<const Block*>(&s)) {
for (auto& st : p->stmts) gen_stmt(*st);
} else if (auto* p = dynamic_cast<const FnDecl*>(&s)) {
emit(Op::FUNC_BEGIN, {}, {}, {}, "", p->name);
for (auto& st : dynamic_cast<const Block*>(p->body.get())->stmts) {
gen_stmt(*st);
}
emit(Op::FUNC_END, {}, {}, {}, "", p->name);
} else if (auto* p = dynamic_cast<const ReturnStmt*>(&s)) {
if (p->value) {
auto v = gen_expr(**p->value);
emit(Op::RET, {}, v);
} else {
emit(Op::RET, {}, Operand::lit(0));
}
} else if (auto* p = dynamic_cast<const ExprStmt*>(&s)) {
gen_expr(*p->expr);
}
}
};

5.6 fib.mini 的 TAC 输出

对开头的 fib 例子,CodeGen 生成的 TAC 序列大致是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FUNC_BEGIN fib
t0 = n < 2
IFGOTO L0, t0, ==, 0
t1 = n
RET t1
GOTO L1
L0:
PARAM n - 1
t2 = CALL fib
PARAM n - 2
t3 = CALL fib
t4 = t2 + t3
RET t4
L1:
FUNC_END fib

LET n = 10
LET i = 0
L2:
t5 = i < n
IFGOTO L3, t5, ==, 0
PARAM i
t6 = CALL fib
PRINTI t6
t7 = i + 1
i = t7
GOTO L2
L3:

5.7 SSA 与 φ 节点

SSA(Static Single Assignment) 的核心约束:每个变量只被赋值一次

问题:在 if/else 分支合并点,变量应该取哪个值?

1
2
3
4
5
6
7
int x;
if (cond) {
x = 1; // 路径 A
} else {
x = 2; // 路径 B
}
// 这里 x 应该是 1 或 2?

φ 节点(phi function)解决:插入 x = phi(x_A, x_B),运行时根据控制流来源选择。

LLVM IR 真实样例(用 clang -emit-llvm -S -O0 编译 int foo(int a, int b, int c) { return a + b * c; }):

1
2
3
4
5
6
define i32 @foo(i32 %a, i32 %b, i32 %c) {
entry:
%mul = mul nsw i32 %b, %c
%add = add nsw i32 %a, %mul
ret i32 %add
}

关键观察每个变量都以 % 开头、只能赋值一次——这就是 SSA。

5.8 LLVM IR 实操:编译一个表达式

1
2
3
4
5
6
7
8
9
10
11
12
# 把 a + b * c 编译成 LLVM IR
$ cat expr.c
int foo(int a, int b, int c) { return a + b * c; }

$ clang -O0 -S -emit-llvm expr.c -o expr.ll
$ cat expr.ll
define i32 @foo(i32 %a, i32 %b, i32 %c) {
entry:
%mul = mul nsw i32 %b, %c
%add = add nsw i32 %a, %mul
ret i32 %add
}

LLVM IR 的 6 个关键术语

术语含义
define函数定义
i3232 位整数(i8, i64, float, double, ptr
%mulSSA 虚拟寄存器(临时变量)
nswNo Signed Wrap(无符号溢出)
entry:入口基本块(Basic Block)标签
ret返回指令

LLVM IR 是真正的”中间表示”——第 4 篇我们将用 LLVM API 重写整个 mini 编译器,生成这些 IR。


六、整合与运行

6.1 main 函数:完整流水线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// ================ main + 测试 ================
#include <iostream>
#include <fstream>
#include <sstream>

std::string read_file(const std::string& path) {
std::ifstream f(path);
if (!f) throw std::runtime_error("Cannot open file: " + path);
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}

// 打印 TAC(调试用)
void print_tac(const std::vector<Instr>& code) {
for (const auto& i : code) {
switch (i.op) {
case Op::ASSIGN:
std::cout << i.dst.to_string() << " = " << i.src1.to_string() << "\n"; break;
case Op::BINOP:
std::cout << i.dst.to_string() << " = " << i.src1.to_string()
<< " " << i.binop << " " << i.src2.to_string() << "\n"; break;
case Op::UNARYOP:
std::cout << i.dst.to_string() << " = " << i.binop
<< i.src1.to_string() << "\n"; break;
case Op::LABEL:
std::cout << i.dst.to_string() << "\n"; break;
case Op::GOTO:
std::cout << "goto " << i.dst.to_string() << "\n"; break;
case Op::IFGOTO:
std::cout << "if " << i.src1.to_string() << " " << i.binop
<< " 0 goto " << i.dst.to_string() << "\n"; break;
case Op::PARAM:
std::cout << "param " << i.src1.to_string() << "\n"; break;
case Op::CALL:
std::cout << i.dst.to_string() << " = call " << i.func_name << "\n"; break;
case Op::RET:
std::cout << "ret " << i.src1.to_string() << "\n"; break;
case Op::PRINTI:
std::cout << "printi " << i.src1.to_string() << "\n"; break;
case Op::PRINTS:
std::cout << "prints " << i.src1.to_string() << "\n"; break;
case Op::FUNC_BEGIN:
std::cout << "func " << i.func_name << ":\n"; break;
case Op::FUNC_END:
std::cout << "endfunc " << i.func_name << "\n"; break;
}
}
}

// 解释执行 TAC(极简:只支持数字与 if/while)
class Interpreter {
public:
long long run(const std::vector<Instr>& code) {
size_t pc = 0;
std::unordered_map<std::string, long long> vars;
std::vector<std::string> call_stack;
while (pc < code.size()) {
const auto& i = code[pc];
switch (i.op) {
case Op::FUNC_BEGIN: call_stack.push_back(i.func_name); break;
case Op::FUNC_END: call_stack.pop_back(); break;
case Op::ASSIGN: vars[i.dst.name] = eval(i.src1, vars, code, pc); break;
case Op::PRINTI: std::cout << eval(i.src1, vars, code, pc) << "\n"; break;
case Op::PRINTS: std::cout << i.src1.str_val << "\n"; break;
case Op::GOTO: pc = label_to_pc(i.dst.name); continue;
case Op::IFGOTO: {
long long v = eval(i.src1, vars, code, pc);
bool take = (i.binop == "==") ? (v == 0) : (v != 0);
if (take) { pc = label_to_pc(i.dst.name); continue; }
break;
}
case Op::RET:
if (call_stack.empty() || call_stack.back() == "main") {
return eval(i.src1, vars, code, pc);
}
break;
default: break;
}
pc++;
}
return 0;
}

// 构建标签到 PC 的映射
void build_labels(const std::vector<Instr>& code) {
labels_.clear();
for (size_t i = 0; i < code.size(); ++i) {
if (code[i].op == Op::LABEL) labels_[code[i].dst.name] = i;
}
}

private:
std::unordered_map<std::string, size_t> labels_;

size_t label_to_pc(const std::string& name) {
// 去掉冒号
std::string n = name;
if (!n.empty() && n.back() == ':') n.pop_back();
return labels_.count(n) ? labels_[n] : 0;
}

long long eval(const Operand& o, const std::unordered_map<std::string, long long>& vars,
const std::vector<Instr>& code, size_t pc) {
switch (o.kind) {
case Operand::INT_LIT: return o.int_val;
case Operand::VAR: {
auto it = vars.find(o.name);
return it != vars.end() ? it->second : 0;
}
case Operand::TEMP: {
auto it = vars.find(o.name);
return it != vars.end() ? it->second : 0;
}
default: return 0;
}
}
};

int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <source.mini>\n";
return 1;
}

try {
// 1. 读源文件
std::string source = read_file(argv[1]);

// 2. 词法分析
Lexer lexer(source);
auto tokens = lexer.tokenize();
std::cout << "=== Tokens ===\n";
for (auto& t : tokens) {
std::cout << " [" << static_cast<int>(t.type) << "] " << t.lexeme << "\n";
}

// 3. 语法分析
Parser parser(std::move(tokens));
Program prog = parser.parse_program();
std::cout << "=== AST ===\n" << prog.to_string() << "\n";

// 4. 语义分析
SemanticAnalyzer sema;
sema.analyze(prog);
std::cout << "=== Semantic Analysis OK ===\n\n";

// 5. IR 生成
CodeGen gen;
auto ir = gen.generate(prog);
std::cout << "=== TAC IR ===\n";
print_tac(ir);
std::cout << "\n=== Running ===\n";

// 6. 解释执行
Interpreter interp;
interp.build_labels(ir);
long long ret = interp.run(ir);
std::cout << "\n=== Exit code: " << ret << " ===\n";

} catch (const std::exception& e) {
std::cerr << "ERROR: " << e.what() << "\n";
return 1;
}
return 0;
}

6.2 编译与运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
$ clang++ -std=c++17 -Wall -Wextra -O2 minilang_compiler.cpp -o minicomp

# 测试 fib
$ cat fib.mini
fn fib(n) {
if (n < 2) {
return n;
} else {
return fib(n-1) + fib(n-2);
}
}
let i = 0;
while (i < 10) {
print(fib(i));
i = i + 1;
}

$ ./minicomp fib.mini
=== Tokens ===
... (略)
=== AST ===
fn fib(n) {
if ((n < 2)) {
return n;
} else {
return (fib((n - 1)) + fib((n - 2)));
}
}
let i = 0;
while ((i < 10)) {
print(fib(i));
i = (i + 1);
}

=== TAC IR ===
... (略)
=== Running ===
0
1
1
2
3
5
8
13
21
34
=== Exit code: 0 ===

6.3 测试用例 1:四则运算 + 变量

1
2
3
4
5
6
7
// arith.mini
let a = 10;
let b = 20;
let c = a + b * 2;
print(c);
let d = (a + b) * 2;
print(d);

预期输出

1
2
50
60

6.4 测试用例 2:函数 + 递归(已演示 fib)

6.5 测试用例 3:if/while 控制流

1
2
3
4
5
6
7
8
9
10
// control.mini
let x = 0;
while (x < 5) {
if (x == 3) {
print("three");
} else {
print(x);
}
x = x + 1;
}

预期输出

1
2
3
4
5
0
1
2
three
4

6.6 错误信息展示

1
2
3
4
// error.mini
let x = 10;
let y = "hello";
let z = x - y; // 类型错误

当前输出(简化版,仅位置):

1
[Semantic Error] line 3: incompatible types in binary '-'

生产编译器(Clang)的错误信息:会指出准确位置期望类型实际类型修复建议。这是工业界 30 年打磨的产物。


七、生产工具横向对比

7.1 选型决策流程

flowchart TD
    Q1{"需要支持多语言<br/>或商业产品?"}:::q
    Q1 -->|"是"| A1["✅ ANTLR4"]:::a
    Q1 -->|"否"| Q2{"主要用 Python?"}:::q
    Q2 -->|"是"| A2["✅ PLY 或 Lark"]:::a
    Q2 -->|"否"| Q3{"需要做完整<br/>编译器后端?"}:::q
    Q3 -->|"是"| A3["✅ LLVM"]:::a
    Q3 -->|"否"| A4["✅ 手写递归下降"]:::a

    style Q1 fill:#FFF9C4,stroke:#F9A825,color:#333
    style Q2 fill:#FFF9C4,stroke:#F9A825,color:#333
    style Q3 fill:#FFF9C4,stroke:#F9A825,color:#333
    style A1 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style A2 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style A3 fill:#B5EAD7,stroke:#80CBC4,color:#333
    style A4 fill:#B5EAD7,stroke:#80CBC4,color:#333

7.2 4 大工具对比表

维度手写(本文)ANTLR4PLY (Python Lex-Yacc)LarkLLVM
语言C++17Java(生成任意语言)PythonPythonC++
词法手写 DFA自动自动(t_xxx 模式)自动(regex / 上下文相关)手写
语法手写递归下降LL(*) 自动LALR 自动Earley / LALR 自动手写递归下降
AST自定义自动 + Visitor手写自动(Tree builder)自定义 + IRBuilder
类型检查手写Visitor手写手写手写
IR 生成TACVisitor 翻译手写手写LLVM IR(自动)
优化几十个 Pass
代码生成x86/ARM/RISC-V/WASM
学习曲线⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
适用规模教学中大型小到中小到中大型 / 工业级
典型用户教学、嵌入式SQL 引擎、Hive教学脚本配置文件解析Clang, Rust

7.3 ANTLR4:MiniLang 文法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// MiniLang.g4 - ANTLR4 文法
grammar MiniLang;

program : stmt* EOF ;
stmt : letStmt | fnDecl | ifStmt | whileStmt | returnStmt | printStmt | exprStmt ;

letStmt : 'let' IDENT '=' expr ';' ;
fnDecl : 'fn' IDENT '(' params? ')' block ;
params : IDENT (',' IDENT)* ;
ifStmt : 'if' '(' expr ')' block ('else' block)? ;
whileStmt: 'while' '(' expr ')' block ;
returnStmt: 'return' expr? ';' ;
printStmt: 'print' '(' expr ')' ';' ;
exprStmt : expr ';' ;
block : '{' stmt* '}' ;

expr : equality ;
equality : comparison (('==' | '!=') comparison)* ;
comparison: additive (('<' | '>' | '<=' | '>=') additive)* ;
additive : multiplicative (('+' | '-') multiplicative)* ;
multiplicative: unary (('*' | '/') unary)* ;
unary : ('!' | '-') unary | primary ;
primary : INT | STRING | 'true' | 'false' | IDENT | call | '(' expr ')' ;
call : IDENT '(' args? ')' ;
args : expr (',' expr)* ;

INT : [0-9]+ ;
STRING : '"' (~["\\] | '\\' .)* '"' ;
IDENT : [a-zA-Z_][a-zA-Z_0-9]* ;
WS : [ \t\r\n]+ -> skip ;

ANTLR4 的杀手锏LL(*) 自适应预测——可以用 * 通配任意前看(不是固定 k),文法表达能力远超传统 LL(k)。

7.4 PLY:MiniLang 词法 + 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# minilang_ply.py
import ply.lex as lex
import ply.yacc as yacc

# ===== 词法 =====
tokens = ('LET', 'FN', 'IF', 'ELSE', 'WHILE', 'RETURN', 'PRINT',
'IDENT', 'INT', 'STRING',
'PLUS', 'MINUS', 'STAR', 'SLASH',
'EQ', 'NEQ', 'LT', 'GT', 'LE', 'GE',
'AND', 'OR', 'BANG', 'ASSIGN',
'LPAREN', 'RPAREN', 'LBRACE', 'RBRACE', 'COMMA', 'SEMI')

t_PLUS = r'\+'; t_MINUS = r'-'; t_STAR = r'\*'; t_SLASH = r'/'
t_EQ = r'=='; t_NEQ = r'!='; t_LE = r'<='; t_GE = r'>='
t_AND = r'&&'; t_OR = r'\|\|'
t_LT = r'<'; t_GT = r'>'; t_BANG = r'!'
t_ASSIGN = r'='
t_LPAREN = r'\('; t_RPAREN = r'\)'
t_LBRACE = r'\{'; t_RBRACE = r'\}'
t_COMMA = r','; t_SEMI = r';'

reserved = {
'let': 'LET', 'fn': 'FN', 'if': 'IF', 'else': 'ELSE',
'while': 'WHILE', 'return': 'RETURN', 'print': 'PRINT',
}

def t_IDENT(t):
r'[a-zA-Z_][a-zA-Z_0-9]*'
t.type = reserved.get(t.value, 'IDENT')
return t

def t_INT(t):
r'\d+'
t.value = int(t.value)
return t

def t_STRING(t):
r'"([^"\\]|\\.)*"'
t.value = t.value[1:-1]
return t

def t_newline(t):
r'\n+'
t.lexer.lineno += len(t.value)

t_ignore = ' \t\r'

def t_error(t):
print(f"Illegal char {t.value[0]!r}")
t.lexer.skip(1)

lexer = lex.lex()

# ===== 语法 =====
def p_program(p):
'program : stmt_list'
p[0] = ('program', p[1])

def p_stmt_list(p):
'''stmt_list : stmt
| stmt_list stmt'''
p[0] = [p[1]] if len(p) == 2 else p[1] + [p[2]]

def p_let_stmt(p):
'stmt : LET IDENT ASSIGN expr SEMI'
p[0] = ('let', p[2], p[4])

def p_expr_binop(p):
'''expr : expr PLUS expr
| expr MINUS expr
| expr STAR expr
| expr SLASH expr'''
p[0] = ('binop', p[2], p[1], p[3])

def p_expr_int(p):
'expr : INT'
p[0] = ('int', p[1])

def p_expr_ident(p):
'expr : IDENT'
p[0] = ('var', p[1])

parser = yacc.yacc()

7.5 Lark:MiniLang 极简 EBNF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# minilang_lark.py
from lark import Lark

# Lark 的杀手锏:纯 EBNF,无需任何 Python 代码
mini_grammar = r"""
program: stmt*
stmt: let_stmt | fn_decl | if_stmt | while_stmt | return_stmt | print_stmt | expr_stmt
let_stmt: "let" NAME "=" expr ";"
fn_decl: "fn" NAME "(" [params] ")" block
params: NAME ("," NAME)*
if_stmt: "if" "(" expr ")" block ["else" block]
while_stmt: "while" "(" expr ")" block
return_stmt: "return" [expr] ";"
print_stmt: "print" "(" expr ")" ";"
expr_stmt: expr ";"
block: "{" stmt* "}"

expr: equality
?equality: comparison (("==" | "!=") comparison)*
?comparison: additive (("<" | ">" | "<=" | ">=") additive)*
?additive: multiplicative (("+" | "-") multiplicative)*
?multiplicative: unary (("*" | "/") unary)*
?unary: ("!" | "-") unary | primary
?primary: INT | STRING | "true" | "false" | NAME | call | "(" expr ")"
call: NAME "(" [args] ")"
args: expr ("," expr)*

INT: /\d+/
STRING: /"[^"]*"/
NAME: /[a-zA-Z_]\w*/
%ignore " " | "\t" | "\n"
"""

parser = Lark(mini_grammar, parser='earley')
ast = parser.parse("let x = 1 + 2;")
print(ast.pretty())

7.6 LLVM:C++ API 写一个表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 使用 LLVM API 生成 IR(需要链接 llvm 库)
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Module.h>

void emit_llvm_ir() {
llvm::LLVMContext ctx;
llvm::Module* mod = new llvm::Module("minic", ctx);
llvm::IRBuilder<> builder(ctx);

// int foo(int a, int b, int c) { return a + b * c; }
std::vector<llvm::Type*> params(3, builder.getInt32Ty());
llvm::FunctionType* fn_ty = llvm::FunctionType::get(builder.getInt32Ty(), params, false);
llvm::Function* foo = llvm::Function::Create(fn_ty, llvm::Function::ExternalLinkage, "foo", mod);

llvm::BasicBlock* entry = llvm::BasicBlock::Create(ctx, "entry", foo);
builder.SetInsertPoint(entry);

auto a = foo->args().begin();
auto b = std::next(a);
auto c = std::next(b);
a->setName("a"); b->setName("b"); c->setName("c");

llvm::Value* mul = builder.CreateMul(b, c, "mul");
llvm::Value* add = builder.CreateAdd(a, mul, "add");
builder.CreateRet(add);

mod->print(llvm::errs(), nullptr);
}

输出

1
2
3
4
5
6
7
8
9
; ModuleID = 'minic'
source_filename = "minic"

define i32 @foo(i32 %a, i32 %b, i32 %c) {
entry:
%mul = mul i32 %b, %c
%add = add i32 %a, %mul
ret i32 %add
}

LLVM API 的优势IRBuilder 帮你处理 SSA 自动命名——你不用手动管理临时变量名,也不会冲突。

7.7 “我该选谁?”决策表

场景推荐工具理由
教学 / 学习编译原理✅ 手写(本文)唯一能让你理解 DFA/Pratt/SSA 的方式
小项目 / 个人玩具语言✅ Lark纯 EBNF、Python 友好,半天能跑通
Python 内部 DSL✅ PLY标准选择,但比 Lark 老
商业 DSL / SQL 引擎✅ ANTLR4工业级、文档完善、错误信息友好
新语言(Rust / Swift / Kotlin Native)✅ LLVM唯一选择——给你免费的多后端、多优化
JIT 编译✅ LLVM ORC / CraneliftLLVM 生态最成熟
WASM 后端✅ LLVMWASM 是 LLVM 支持的目标之一

八、动手实验 + 思考题

8.1 7 道递进思考题

#难度题目
1在 Lexer 里加 %(取模)运算符,需要改哪些地方?
2让 Parser 支持数组字面量 [1, 2, 3],AST 加什么节点?
3⭐⭐if 条件支持 0 为假、非 0 为真的隐式转换,在 SemanticAnalyzer 哪里改?
4⭐⭐实现 let x = if (cond) { 1 } else { 2 };——这是表达式还是语句?
5⭐⭐⭐实现 for 循环——比 while 多 1 个初始化语句 + 1 个迭代表达式
6⭐⭐⭐实现 闭包——挑战:捕获栈上的变量,要把它”逃逸”到堆上
7⭐⭐⭐⭐用本文的 AST 节点,直接生成 LLVM IR(第 4 篇的预告)

8.2 推荐阅读

资源类型价值
《编译原理》(龙书) Alfred V. Aho et al.教科书编译原理圣经——所有概念的标准定义
《现代编译原理》(虎书) Andrew W. Appel教科书更现代、有 ML 项目贯穿
《Engineering a Compiler》 Cooper & Torczon教科书工程视角,比龙书易读
LLVM Tutorial llvm.org/docs/tutorial实战手把手写 Kaleidoscope——第 4 篇的预热
Crafting Interpreters craftinginterpreters.com实战Robert Nystrom 的两本合一——lox 解释器
ANTLR Mega Tutorial实战tomassetti.me/antlr-mega-tutorial
GCC Internals gcc.gnu.org/onlinedocs/gccint源码想读 GCC 源码必看

8.3 三段行动建议

如果你刚学编译原理

不要从 LLVM 开始读。先手写 4 阶段——把本文的 800 行代码逐行重写一遍,加几个新特性(%for、数组),彻底理解 DFA、AST、符号表、IR 的关系。

如果你已会编译原理,想做项目

选一个 DSL 用 PLY/Lark 实现。比如:

  • JSON Schema(带类型检查)
  • SQL 子集(带 WHERE/JOIN)
  • Markdown 扩展(带自定义标签)
    这些项目1 周内可完成,且能直接用于生产。

如果你想读 LLVM 源码

第 4 篇就是为你写的。我们会用 LLVM API 重新实现本文的 mini 编译器,让你看到:

  • clang::Sema = 我们的 SemanticAnalyzer
  • clang::CodeGen = 我们的 CodeGen
  • llvm::IRBuilder = 自动 SSA 命名的 gen_expr
    目标:把 800 行手写代码映射到 LLVM 100 万行源码的关键函数

九、本文小结

9.1 核心结论

编译器前端 = 4 个独立阶段的串联——每个阶段都有自己的数据格式和算法;每个阶段都把”上阶段的复杂结构”翻译成”下阶段更易处理的形式”。

graph LR
    C["字符流"]:::p1
    T["Token 流"]:::p2
    A["AST"]:::p3
    S["带类型 AST"]:::p4
    I["三地址码 / SSA"]:::p5

    C ==>|"Lexer<br/>(DFA)"| T
    T ==>|"Parser<br/>(LL/Pratt)"| A
    A ==>|"Sema<br/>(符号表)"| S
    S ==>|"CodeGen<br/>(递归翻译)"| I

    classDef p1 fill:#C7CEEA,stroke:#9FA8DA,color:#333
    classDef p2 fill:#E8D5F5,stroke:#CE93D8,color:#333
    classDef p3 fill:#FFDAB9,stroke:#FFAB76,color:#333
    classDef p4 fill:#FFF9C4,stroke:#F9A825,color:#333
    classDef p5 fill:#B5EAD7,stroke:#80CBC4,color:#333

9.2 知识点回顾

知识点状态关键代码
DFA 状态机✅ 掌握Lexer 类(150 行)
最长匹配✅ 掌握read_ident_or_keyword
Pratt 表达式解析✅ 掌握parse_expr(min_bp)
AST 节点设计✅ 掌握12 个具体类
栈式符号表✅ 掌握SymbolTable::enter/exit_scope
三地址码 TAC✅ 掌握CodeGen::gen_expr
LLVM IR 桥接✅ 掌握第 4 篇预告
SSA 概念✅ 掌握φ 节点、虚拟寄存器

9.3 本文在系列中的位置

graph LR
    A["本文:4 阶段全打通"]:::here
    B["第 2 篇<br/>优化 Pass"]:::next
    C["第 3 篇<br/>x86-64 后端"]:::next
    D["第 4 篇<br/>LLVM API"]:::next
    E["第 5 篇<br/>JIT"]:::next

    A ==> B ==> C ==> D ==> E

    style A fill:#FFB3C6,stroke:#F48FB1,stroke-width:4px,color:#333
    style B fill:#C7CEEA,stroke:#9FA8DA,color:#333
    style C fill:#E8D5F5,stroke:#CE93D8,color:#333
    style D fill:#FFDAB9,stroke:#FFAB76,color:#333
    style E fill:#B5EAD7,stroke:#80CBC4,color:#333

9.4 进一步思考

为什么编译器要分 4 阶段?
因为每个阶段的”关注点”不同

  • 词法关注”字符 → 词”——和源文件格式相关。
  • 语法关注”词 → 树”——和语言文法相关。
  • 语义关注”名字 → 含义”——和作用域/类型相关。
  • IR关注”含义 → 计算”——和 CPU 架构无关

这 4 层的正交分解让 GCC 支撑 7 种语言、Clang 支撑 7 个后端、LLVM 支撑 30+ 种语言。没有这 4 层抽象,就没有现代编译器生态。


📚 编译原理实战 系列导航

本文是《编译原理实战》系列第 1/5 篇。

#文章状态难度
1本文:4 阶段全打通✅ 已发布⭐⭐⭐
2优化 Pass:常量折叠、死代码消除、循环优化🔜 计划中⭐⭐⭐⭐
3目标代码生成:x86-64 后端🔜 计划中⭐⭐⭐⭐
4LLVM 实战:用 LLVM API 重写本文 mini 编译器🔜 计划中⭐⭐⭐⭐⭐
5JIT 编译:运行时编译与 HotSpot🔜 计划中⭐⭐⭐⭐⭐
📖 全部 5 篇目录(点击展开)
  1. 第 1 篇:手写 C++17 编译器前端——词法、语法、语义、IR 4 阶段全打通 ← 当前
  2. 第 2 篇:优化 Pass——常量折叠、死代码消除、循环优化
  3. 第 3 篇:目标代码生成——x86-64 后端
  4. 第 4 篇:LLVM 实战——用 LLVM API 重写 mini 编译器
  5. 第 5 篇:JIT 编译——运行时编译与 HotSpot

最后一句话:编译原理不是”龙书里的抽象数学”——它是DFA + 递归下降 + 栈式符号表 + 三地址码这 4 个具体工程模式的组合。一旦你亲手把这 4 个模式各写一遍,GCC 100 万行、Clang 80 万行、LLVM 100 万行代码都只是这 4 个模式的变体——再也不是黑盒。