一句话核心结论 :printf("Hello, World!") 这一行 C 代码走完 GCC 完整编译,会经过 4 个独立阶段、跨越 6 个数据格式、调用 30+ 个内部 API 。本文用 800 行 C++17 代码,把这 4 个阶段全部手写一遍——从此 DFA、AST、SSA 不再是课本术语,而是你能跑、能改、能调的工程实现。
系列导航 # 文章 状态 1 本文:4 阶段全打通 ✅ 已发布 2 优化 Pass:常量折叠、死代码消除、循环优化 🔜 计划中 3 目标代码生成:x86-64 后端 🔜 计划中 4 LLVM 实战:用 LLVM API 重写本文 mini 编译器 🔜 计划中 5 JIT 编译:运行时编译与 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(默认数字)、bool、string、function字面量 整数、字符串("...")、布尔 语句 let 声明、函数定义、if/while、return、print、表达式语句特性 类型自动推导、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 手写递归下降 手写 + 库化 Sema LLVM 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 完整 IR std::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 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 }; 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 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 中文 = 1 C++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 LALR Lookahead LR LR 的简化版 工业主流 Yacc, Bison Pratt Top-Down Operator Precedence LL 的扩展,按优先级分发 表达式解析最优雅 (手写,无工具)
当前编译器主流 :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) - c;a = 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 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 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 ; } } 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 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; } 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 (); 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.0 是 int 还是 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, 0bool布尔 true, falsestring字符串 "hello", ""function函数 (无字面量,从 fn 推导) void无返回值 (仅用于函数返回类型) error错误类型 (类型检查失败占位)
4.5 隐式转换规则 from \ to int bool string 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; return t; } void error (const Token& tk, const std::string& msg) { std::cerr << "[Semantic Error] line " << tk.line << ": " << msg << "\n" ; errors_++; } 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}); } if (dynamic_cast <const Call*>(&e)) { return std::make_shared <Type>((Type){TypeKind::INT}); } 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 错误检测 控制流分析 AST CFG(基本块 + 边) 优化基础 活跃性分析 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:#333N 个语言 × M 个目标 :没有 IR 需要 N×M 个编译器;有 IR 只需 N+M 个。
5.2 三种 IR 范式 范式 全称 特点 代表 栈式 IR Stack-based 像 JVM 字节码:操作数在栈上 JVM 字节码 、WebAssembly 、CPython bytecode三地址码(TAC) Three-Address Code 每条指令最多 3 个操作数:x = y op z GCC GIMPLE (雏形)、本文 mini 编译器SSA Static Single Assignment 每个变量只赋值一次,带 φ 节点 LLVM IR 、V8 TurboFan 、rustc 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 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 "?" ; } }; 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; Operand dst, src1, src2; std::string func_name; std::vector<Operand> args; }; 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); 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" ); 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 ; } else { x = 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 $ 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 #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 (); } 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 ; } } } 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 ; } 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 { std::string source = read_file (argv[1 ]); 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" ; } Parser parser (std::move(tokens)) ; Program prog = parser.parse_program (); std::cout << "=== AST ===\n" << prog.to_string () << "\n" ; SemanticAnalyzer sema; sema.analyze (prog); std::cout << "=== Semantic Analysis OK ===\n\n" ; CodeGen gen; auto ir = gen.generate (prog); std::cout << "=== TAC IR ===\n" ; print_tac (ir); std::cout << "\n=== Running ===\n" ; 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 $ 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);
预期输出 :
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; }
预期输出 :
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:#3337.2 4 大工具对比表 维度 手写(本文) ANTLR4 PLY (Python Lex-Yacc) Lark LLVM 语言 C++17 Java(生成任意语言) Python Python C++ 词法 手写 DFA 自动 自动(t_xxx 模式) 自动(regex / 上下文相关) 手写 语法 手写递归下降 LL(*) 自动 LALR 自动 Earley / LALR 自动 手写递归下降 AST 自定义 自动 + Visitor 手写 自动(Tree builder) 自定义 + IRBuilder 类型检查 手写 Visitor 手写 手写 手写 IR 生成 TAC Visitor 翻译 手写 手写 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 import ply.lex as leximport ply.yacc as yacctokens = ('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 from lark import Larkmini_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 #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); 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 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 / Cranelift LLVM 生态最成熟 WASM 后端 ✅ LLVM WASM 是 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 推荐阅读 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 = 我们的 SemanticAnalyzerclang::CodeGen = 我们的 CodeGenllvm::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:#3339.2 知识点回顾 知识点 状态 关键代码 DFA 状态机 ✅ 掌握 Lexer 类(150 行)最长匹配 ✅ 掌握 read_ident_or_keywordPratt 表达式解析 ✅ 掌握 parse_expr(min_bp)AST 节点设计 ✅ 掌握 12 个具体类 栈式符号表 ✅ 掌握 SymbolTable::enter/exit_scope三地址码 TAC ✅ 掌握 CodeGen::gen_exprLLVM 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:#3339.4 进一步思考 为什么编译器要分 4 阶段? 因为每个阶段的”关注点”不同 :
词法 关注”字符 → 词”——和源文件格式相关。语法 关注”词 → 树”——和语言文法相关。语义 关注”名字 → 含义”——和作用域/类型相关。IR 关注”含义 → 计算”——和 CPU 架构无关 。这 4 层的正交分解 让 GCC 支撑 7 种语言、Clang 支撑 7 个后端、LLVM 支撑 30+ 种语言。没有这 4 层抽象,就没有现代编译器生态。
📚 编译原理实战 系列导航 本文是《编译原理实战》系列第 1/5 篇。
# 文章 状态 难度 1 本文:4 阶段全打通 ✅ 已发布 ⭐⭐⭐ 2 优化 Pass:常量折叠、死代码消除、循环优化 🔜 计划中 ⭐⭐⭐⭐ 3 目标代码生成:x86-64 后端 🔜 计划中 ⭐⭐⭐⭐ 4 LLVM 实战:用 LLVM API 重写本文 mini 编译器 🔜 计划中 ⭐⭐⭐⭐⭐ 5 JIT 编译:运行时编译与 HotSpot 🔜 计划中 ⭐⭐⭐⭐⭐
📖 全部 5 篇目录(点击展开) 第 1 篇:手写 C++17 编译器前端——词法、语法、语义、IR 4 阶段全打通 ← 当前第 2 篇:优化 Pass——常量折叠、死代码消除、循环优化 第 3 篇:目标代码生成——x86-64 后端 第 4 篇:LLVM 实战——用 LLVM API 重写 mini 编译器 第 5 篇:JIT 编译——运行时编译与 HotSpot 最后一句话 :编译原理不是”龙书里的抽象数学”——它是DFA + 递归下降 + 栈式符号表 + 三地址码 这 4 个具体工程模式的组合。一旦你亲手把这 4 个模式各写一遍 ,GCC 100 万行、Clang 80 万行、LLVM 100 万行代码都只是这 4 个模式的变体 ——再也不是黑盒。