编译原理完整知识总结与使用教程
目录
- 编译器概述与整体架构
- [词法分析(Lexical Analysis)](#词法分析(Lexical Analysis))
- [语法分析(Syntax Analysis / Parsing)](#语法分析(Syntax Analysis / Parsing))
- [语义分析(Semantic Analysis)](#语义分析(Semantic Analysis))
- 抽象语法树(AST)
- 中间表示(IR)
- 编译优化(Optimization)
- [代码生成(Code Generation)](#代码生成(Code Generation))
- 工具链与实践教程
- 实战示例:从零构建一个简单编译器
- 现代编译器技术前沿
- 参考资源
1. 编译器概述与整体架构
1.1 什么是编译器
编译器(Compiler)是一种将**高级语言(源语言)翻译为低级语言(目标语言)**的软件工具。它不改变程序的语义,只是改变其表示形式,同时尽可能生成高效、优化的目标代码。
源代码 → [编译器] → 目标代码(机器码 / 汇编 / 字节码)
编译器与解释器的主要区别:
- 编译器:先整体翻译,再执行(C、C++、Rust、Go)
- 解释器:边翻译边执行(Python、早期 JavaScript)
- 混合模式:先编译为字节码,再由 JIT 解释或编译执行(Java、Python CPython、V8)
1.2 编译器的整体架构
现代编译器通常分为前端(Front-End) 、中端(Middle-End) 和**后端(Back-End)**三个部分:
┌─────────────────────────────────────────────────────────────────┐
│ 编译器整体架构 │
│ │
│ 源代码 │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ 前端 (Front-End) │ │
│ │ 词法分析 → 语法分析 → 语义分析 → IR生成 │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ 中端 (Middle-End) │ │
│ │ 与平台无关的IR优化 │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────┐ │
│ │ 后端 (Back-End) │ │
│ │ 指令选择 → 寄存器分配 → 指令调度 → 目标码 │ │
│ └─────────────────────────────────┘ │
│ ↓ │
│ 目标代码(汇编/机器码) │
└─────────────────────────────────────────────────────────────────┘
1.3 编译的六大阶段(Phase)
| 阶段 | 输入 | 输出 | 主要任务 |
|---|---|---|---|
| 词法分析 | 字符流 | Token 流 | 识别词素,分类 Token |
| 语法分析 | Token 流 | 语法树 / AST | 按语法规则建树 |
| 语义分析 | AST | 注解 AST + 符号表 | 类型检查、作用域分析 |
| IR 生成 | 注解 AST | 中间表示(IR) | 转为平台无关 IR |
| 优化 | IR | 优化后的 IR | 消除冗余、提升性能 |
| 代码生成 | 优化 IR | 机器码 / 汇编 | 指令选择、寄存器分配 |
2. 词法分析(Lexical Analysis)
2.1 基本概念
词法分析是编译器的第一阶段,其核心任务是将源代码的字符流转换为有意义的**Token(词法单元)**序列。
关键术语
| 术语 | 定义 |
|---|---|
| 词素(Lexeme) | 源代码中匹配某个模式的字符序列(如 int、x、=、10) |
| Token | 词素的抽象表示,包含类型和值,如 <KEYWORD, int>、<IDENTIFIER, x> |
| 模式(Pattern) | 描述 Token 类的规则,通常用正则表达式定义 |
Token 的主要类型
- 关键字(Keywords): if, else, while, for, int, return ...
- 标识符(Identifiers): 变量名、函数名
- 字面量(Literals): 整数 42、浮点 3.14、字符串 "hello"
- 运算符(Operators): +, -, *, /, ==, !=, <=
- 分隔符(Delimiters): (, ), {, }, ;, ,
- 注释(Comments): // ... 或 /* ... */ (通常被丢弃)
2.2 正则表达式与有限自动机
词法分析的理论基础是正则语言 和有限自动机(FA)。
正则表达式常用符号
a 匹配字符 a
a|b 匹配 a 或 b(交替)
ab 匹配 a 后跟 b(连接)
a* 匹配 a 的零次或多次(Kleene 星)
a+ 匹配 a 的一次或多次
a? 匹配 a 的零次或一次
[a-z] 字符类,匹配 a 到 z 中任一字符
[^0-9] 否定字符类,不匹配数字
. 匹配任意字符(除换行)
常用 Token 的正则定义
digit → [0-9]
letter → [a-zA-Z_]
integer → digit+
float → digit+ '.' digit*
identifier → letter (letter | digit)*
whitespace → (' ' | '\t' | '\n')+
有限自动机(FA)类型
NFA(非确定性有限自动机):
- 给定状态和输入,可能有多个转移或 ε 转移
- 通过 Thompson 构造法从正则表达式生成
DFA(确定性有限自动机):
- 给定状态和输入,有唯一转移
- 通过子集构造法(Subset Construction)从 NFA 转换
- 执行效率更高,适合实际词法分析器
NFA → DFA 转换示例(识别 a*b)
NFA:
状态: 0 --ε→ 1, 1 --a→ 1, 1 --ε→ 2, 2 --b→ 3(终态)
DFA(子集构造):
{0,1,2} --a→ {1,2}
{0,1,2} --b→ {3} (接受)
{1,2} --a→ {1,2}
{1,2} --b→ {3} (接受)
2.3 词法分析器的实现
手写词法分析器(C 示例)
c
#include <stdio.h>
#include <ctype.h>
#include <string.h>
typedef enum {
TOKEN_INT, TOKEN_IDENTIFIER, TOKEN_NUMBER,
TOKEN_PLUS, TOKEN_MINUS, TOKEN_STAR, TOKEN_SLASH,
TOKEN_ASSIGN, TOKEN_SEMICOLON, TOKEN_EOF, TOKEN_UNKNOWN
} TokenType;
typedef struct {
TokenType type;
char value[64];
} Token;
const char *source;
int pos = 0;
Token next_token() {
Token tok;
// 跳过空白
while (source[pos] && isspace(source[pos])) pos++;
if (!source[pos]) { tok.type = TOKEN_EOF; return tok; }
char c = source[pos];
// 识别关键字和标识符
if (isalpha(c) || c == '_') {
int i = 0;
while (isalnum(source[pos]) || source[pos] == '_')
tok.value[i++] = source[pos++];
tok.value[i] = '\0';
if (strcmp(tok.value, "int") == 0) tok.type = TOKEN_INT;
else tok.type = TOKEN_IDENTIFIER;
return tok;
}
// 识别整数
if (isdigit(c)) {
int i = 0;
while (isdigit(source[pos]))
tok.value[i++] = source[pos++];
tok.value[i] = '\0';
tok.type = TOKEN_NUMBER;
return tok;
}
// 识别单字符运算符
tok.value[0] = c; tok.value[1] = '\0';
pos++;
switch (c) {
case '+': tok.type = TOKEN_PLUS; break;
case '-': tok.type = TOKEN_MINUS; break;
case '*': tok.type = TOKEN_STAR; break;
case '/': tok.type = TOKEN_SLASH; break;
case '=': tok.type = TOKEN_ASSIGN; break;
case ';': tok.type = TOKEN_SEMICOLON; break;
default: tok.type = TOKEN_UNKNOWN; break;
}
return tok;
}
使用 Flex 生成词法分析器
Flex 是最常用的词法分析器生成工具,规则文件格式为 .l:
lex
%{
#include "tokens.h"
%}
%%
[ \t\n]+ { /* 跳过空白 */ }
"//".* { /* 跳过行注释 */ }
"int" { return TOKEN_INT; }
"if" { return TOKEN_IF; }
"while" { return TOKEN_WHILE; }
"return" { return TOKEN_RETURN; }
[a-zA-Z_][a-zA-Z0-9_]* { yylval.str = strdup(yytext); return TOKEN_ID; }
[0-9]+ { yylval.ival = atoi(yytext); return TOKEN_NUM; }
"+" { return TOKEN_PLUS; }
"-" { return TOKEN_MINUS; }
"*" { return TOKEN_STAR; }
"/" { return TOKEN_SLASH; }
"=" { return TOKEN_ASSIGN; }
"==" { return TOKEN_EQ; }
"!=" { return TOKEN_NEQ; }
"<=" { return TOKEN_LE; }
">=" { return TOKEN_GE; }
";" { return TOKEN_SEMI; }
"(" { return TOKEN_LPAREN; }
")" { return TOKEN_RPAREN; }
"{" { return TOKEN_LBRACE; }
"}" { return TOKEN_RBRACE; }
. { fprintf(stderr, "未知字符: %c\n", yytext[0]); }
%%
int yywrap() { return 1; }
编译与使用:
bash
flex lexer.l # 生成 lex.yy.c
gcc lex.yy.c -lfl -o lexer
2.4 最长匹配原则与优先规则
词法分析器遵循以下两条核心原则:
-
最长匹配(Maximal Munch):在多个候选 Token 中,选择能匹配最长字符串的那个。
- 例如:
<=应被识别为TOKEN_LE,而不是TOKEN_LT后跟TOKEN_ASSIGN
- 例如:
-
优先规则(Rule Priority):当多个规则匹配同样长度时,优先选择先定义的规则。
- 例如:
int优先被识别为关键字,而非标识符
- 例如:
3. 语法分析(Syntax Analysis / Parsing)
3.1 基本概念
语法分析(也称解析 / Parsing )是编译器的第二阶段 ,接收词法分析器产生的 Token 流,按照语言的语法规则(文法)来检查程序结构的正确性,并建立语法树 或抽象语法树(AST)。
3.2 上下文无关文法(CFG)
语法分析使用**上下文无关文法(Context-Free Grammar,CFG)**来描述程序语言的语法结构。
CFG 的四元组 G = (V, T, P, S)
| 组成 | 说明 |
|---|---|
| V | 非终结符集合(Nonterminals),如 Expr、Stmt |
| T | 终结符集合(Terminals),即 Token 类型 |
| P | 产生式规则集合(Productions) |
| S | 开始符号(Start Symbol) |
示例:算术表达式文法
E → E + T | E - T | T
T → T * F | T / F | F
F → ( E ) | id | num
BNF / EBNF 表示法
ebnf
// EBNF(扩展 BNF)示例
program ::= stmt*
stmt ::= assign_stmt | if_stmt | while_stmt | return_stmt
assign_stmt::= ID '=' expr ';'
if_stmt ::= 'if' '(' expr ')' '{' stmt* '}' ('else' '{' stmt* '}')?
while_stmt ::= 'while' '(' expr ')' '{' stmt* '}'
expr ::= expr ('+' | '-') term | term
term ::= term ('*' | '/') factor | factor
factor ::= '(' expr ')' | ID | NUM
3.3 自顶向下解析(Top-Down Parsing)
从开始符号出发,不断展开非终结符,尝试匹配输入的 Token 序列。
LL(1) 文法
- LL(1):Left-to-right scan,Leftmost derivation,1-token lookahead
- 预测解析,无回溯
- 需要构造 FIRST 集 和 FOLLOW 集
FIRST 集:某个符号串可以推导出的句子的第一个终结符集合。
FOLLOW 集:某个非终结符后面可能紧跟的终结符集合。
预测解析表构造算法:
对每条产生式 A → α:
1. 对 FIRST(α) 中的每个终结符 a,将 A → α 加入 M[A, a]
2. 若 ε ∈ FIRST(α),则对 FOLLOW(A) 中的每个终结符 b,
将 A → α 加入 M[A, b]
递归下降解析器(Recursive Descent Parser)
最直观的自顶向下实现方式,每个非终结符对应一个函数:
python
class Parser:
def __init__(self, tokens):
self.tokens = tokens
self.pos = 0
def current(self):
return self.tokens[self.pos] if self.pos < len(self.tokens) else None
def consume(self, expected_type=None):
tok = self.current()
if expected_type and tok.type != expected_type:
raise SyntaxError(f"期望 {expected_type},得到 {tok.type}")
self.pos += 1
return tok
def parse_expr(self):
"""expr → term (('+' | '-') term)*"""
node = self.parse_term()
while self.current() and self.current().type in ('PLUS', 'MINUS'):
op = self.consume()
right = self.parse_term()
node = BinaryOp(op.value, node, right)
return node
def parse_term(self):
"""term → factor (('*' | '/') factor)*"""
node = self.parse_factor()
while self.current() and self.current().type in ('STAR', 'SLASH'):
op = self.consume()
right = self.parse_factor()
node = BinaryOp(op.value, node, right)
return node
def parse_factor(self):
"""factor → '(' expr ')' | NUM | ID"""
tok = self.current()
if tok.type == 'LPAREN':
self.consume('LPAREN')
node = self.parse_expr()
self.consume('RPAREN')
return node
elif tok.type == 'NUMBER':
self.consume()
return NumberLiteral(int(tok.value))
elif tok.type == 'IDENTIFIER':
self.consume()
return Identifier(tok.value)
raise SyntaxError(f"意外的 Token: {tok}")
3.4 自底向上解析(Bottom-Up Parsing)
从叶节点(输入 Token)开始,通过**规约(Reduce)**不断将右部替换为左部非终结符,直到归约为开始符号。
LR 解析族
| 类型 | 全称 | 特点 |
|---|---|---|
| LR(0) | LR with 0 lookahead | 最弱,实际语言几乎不可用 |
| SLR(1) | Simple LR(1) | 利用 FOLLOW 集消歧 |
| LALR(1) | Lookahead LR(1) | 合并同心集,Yacc/Bison 使用 |
| LR(1) | Canonical LR(1) | 最强,但状态数多 |
LR 分析表(移入-规约表)
LR 分析器通过维护一个栈 和状态机来工作:
动作(Action)表:
- 移入(Shift s):将当前输入 Token 和状态 s 压栈
- 规约(Reduce r):按产生式 r 弹栈,压入新状态
- 接受(Accept):解析成功
- 报错(Error):发现语法错误
转移(Goto)表:
- 规约后,根据栈顶状态和刚规约得到的非终结符,确定新状态
解析过程示例(id + id * id):
栈 输入 动作
──────────────────────────────────────
0 id + id * id $ 移入
0 id 5 + id * id $ 规约 F → id
0 F 3 + id * id $ 规约 T → F
0 T 2 + id * id $ 规约 E → T
0 E 1 + id * id $ 移入
0 E 1 + 6 id * id $ 移入
0 E 1 + 6 id 5 * id $ 规约 F → id
...(继续规约)
使用 Yacc / Bison 生成语法分析器
yacc
%{
#include <stdio.h>
#include "ast.h"
ASTNode *root;
%}
%union {
int ival;
char *sval;
ASTNode *node;
}
%token <ival> NUM
%token <sval> ID
%token PLUS MINUS STAR SLASH ASSIGN SEMI
%token IF ELSE WHILE RETURN
%type <node> program stmt expr term factor
%left PLUS MINUS
%left STAR SLASH
%%
program : stmt_list { root = $1; }
stmt_list
: stmt_list stmt { $$ = make_seq($1, $2); }
| stmt { $$ = $1; }
;
stmt
: ID ASSIGN expr SEMI { $$ = make_assign($1, $3); }
| IF '(' expr ')' stmt { $$ = make_if($3, $5, NULL); }
| RETURN expr SEMI { $$ = make_return($2); }
;
expr
: expr PLUS term { $$ = make_binop('+', $1, $3); }
| expr MINUS term { $$ = make_binop('-', $1, $3); }
| term { $$ = $1; }
;
term
: term STAR factor { $$ = make_binop('*', $1, $3); }
| term SLASH factor { $$ = make_binop('/', $1, $3); }
| factor { $$ = $1; }
;
factor
: NUM { $$ = make_num($1); }
| ID { $$ = make_id($1); }
| '(' expr ')' { $$ = $2; }
;
%%
int yyerror(const char *msg) {
fprintf(stderr, "语法错误: %s\n", msg);
return 1;
}
3.5 错误处理与恢复
语法分析器必须能够处理错误并尽量继续解析,以发现更多错误:
错误恢复策略:
- 紧急模式(Panic Mode) :丢弃输入 Token 直到找到一个可以继续解析的同步符号(如
;、}) - 短语级别恢复(Phrase-Level Recovery):对当前处理的短语进行局部修正(插入/删除 Token)
- 错误产生式(Error Productions):在文法中加入常见错误的产生式
- 全局纠错(Global Correction):寻找最少编辑距离的合法程序(代价高,实际少用)
4. 语义分析(Semantic Analysis)
4.1 主要任务
语义分析在语法分析之后进行,检查程序的语义正确性:
- 类型检查(Type Checking):表达式类型是否兼容
- 作用域分析(Scope Analysis):变量使用前是否已声明
- 名称解析(Name Resolution):将标识符绑定到其声明
- 控制流检查 :如
break/continue是否在循环内 - 唯一性检查:同一作用域内不能重复声明变量
4.2 符号表(Symbol Table)
符号表是语义分析中最重要的数据结构,记录程序中所有声明的信息:
c
typedef struct Symbol {
char *name; // 标识符名称
DataType type; // 数据类型
int scope_level; // 作用域层次
int offset; // 栈帧偏移(用于代码生成)
bool is_function; // 是否为函数
struct Symbol *next;// 链表(处理哈希冲突)
} Symbol;
typedef struct SymbolTable {
Symbol *buckets[TABLE_SIZE]; // 哈希表
struct SymbolTable *parent; // 父作用域
int level; // 作用域层次
} SymbolTable;
// 进入新作用域
SymbolTable* enter_scope(SymbolTable *parent) {
SymbolTable *new_scope = malloc(sizeof(SymbolTable));
memset(new_scope->buckets, 0, sizeof(new_scope->buckets));
new_scope->parent = parent;
new_scope->level = parent ? parent->level + 1 : 0;
return new_scope;
}
// 查找符号(支持词法作用域链)
Symbol* lookup(SymbolTable *table, const char *name) {
while (table) {
int h = hash(name) % TABLE_SIZE;
Symbol *sym = table->buckets[h];
while (sym) {
if (strcmp(sym->name, name) == 0) return sym;
sym = sym->next;
}
table = table->parent; // 向外层作用域查找
}
return NULL; // 未找到
}
4.3 类型系统与类型检查
python
class TypeChecker:
def check_binop(self, node):
left_type = self.check(node.left)
right_type = self.check(node.right)
if node.op in ('+', '-', '*', '/'):
if left_type == right_type == 'int':
return 'int'
elif left_type in ('int', 'float') and right_type in ('int', 'float'):
return 'float' # 隐式提升
else:
raise TypeError(f"运算符 '{node.op}' 不支持类型 {left_type} 和 {right_type}")
elif node.op in ('==', '!=', '<', '>', '<=', '>='):
if left_type == right_type:
return 'bool'
raise TypeError(f"比较操作的两端类型不匹配: {left_type} vs {right_type}")
5. 抽象语法树(AST)
5.1 什么是 AST
**抽象语法树(Abstract Syntax Tree,AST)**是源代码语法结构的一种树状表示。与具体语法树(Parse Tree)不同,AST 省略了不必要的语法细节(括号、分号等),只保留程序的逻辑结构。
AST vs Parse Tree 对比
对于表达式 2 * (3 + 4):
Parse Tree(具体语法树): AST(抽象语法树):
E *
| / \
T 2 +
/|\ / \
T * F 3 4
| / \
F ( E )
| |
2 ...(更多节点)
Parse Tree 包含每条文法规则的节点,AST 更紧凑。
5.2 AST 节点设计
Python 实现
python
from dataclasses import dataclass, field
from typing import Optional, List, Any
@dataclass
class ASTNode:
"""AST 基类"""
line: int = 0
col: int = 0
@dataclass
class Program(ASTNode):
statements: List[ASTNode] = field(default_factory=list)
@dataclass
class VarDecl(ASTNode):
name: str = ""
type_name: str = ""
init: Optional[ASTNode] = None
@dataclass
class FuncDecl(ASTNode):
name: str = ""
return_type: str = ""
params: List['Param'] = field(default_factory=list)
body: Optional['Block'] = None
@dataclass
class Param(ASTNode):
name: str = ""
type_name: str = ""
@dataclass
class Block(ASTNode):
statements: List[ASTNode] = field(default_factory=list)
@dataclass
class AssignStmt(ASTNode):
target: ASTNode = None
value: ASTNode = None
@dataclass
class IfStmt(ASTNode):
condition: ASTNode = None
then_block: ASTNode = None
else_block: Optional[ASTNode] = None
@dataclass
class WhileStmt(ASTNode):
condition: ASTNode = None
body: ASTNode = None
@dataclass
class ReturnStmt(ASTNode):
value: Optional[ASTNode] = None
@dataclass
class BinaryOp(ASTNode):
op: str = ""
left: ASTNode = None
right: ASTNode = None
@dataclass
class UnaryOp(ASTNode):
op: str = ""
operand: ASTNode = None
@dataclass
class CallExpr(ASTNode):
callee: str = ""
args: List[ASTNode] = field(default_factory=list)
@dataclass
class Identifier(ASTNode):
name: str = ""
@dataclass
class IntLiteral(ASTNode):
value: int = 0
@dataclass
class FloatLiteral(ASTNode):
value: float = 0.0
@dataclass
class StringLiteral(ASTNode):
value: str = ""
C 实现(Tagged Union)
c
typedef enum {
AST_PROGRAM, AST_VAR_DECL, AST_FUNC_DECL,
AST_BLOCK, AST_ASSIGN, AST_IF, AST_WHILE, AST_RETURN,
AST_BINOP, AST_UNOP, AST_CALL, AST_IDENT, AST_INT, AST_FLOAT
} ASTTag;
typedef struct ASTNode {
ASTTag tag;
int line, col;
union {
struct { struct ASTNode **stmts; int count; } program;
struct { char *name; char *type; struct ASTNode *init; } var_decl;
struct { char *name; char *ret_type;
struct ASTNode **params; int param_count;
struct ASTNode *body; } func_decl;
struct { struct ASTNode *left; struct ASTNode *right; char op[4]; } binop;
struct { char *name; struct ASTNode **args; int argc; } call;
struct { char *name; } ident;
struct { long long val; } integer;
struct { double val; } fp;
struct { struct ASTNode *cond; struct ASTNode *then; struct ASTNode *els; } if_stmt;
struct { struct ASTNode *cond; struct ASTNode *body; } while_stmt;
struct { struct ASTNode *val; } ret_stmt;
} data;
} ASTNode;
5.3 访问者模式(Visitor Pattern)
遍历 AST 最常用的设计模式是访问者模式(Visitor Pattern),它将算法与数据结构解耦,允许在不修改 AST 节点类的情况下添加新的操作。
python
from abc import ABC, abstractmethod
class ASTVisitor(ABC):
"""访问者基类"""
def visit(self, node: ASTNode):
"""分发到具体的 visit_XXX 方法"""
method_name = f'visit_{type(node).__name__}'
visitor = getattr(self, method_name, self.generic_visit)
return visitor(node)
def generic_visit(self, node: ASTNode):
"""默认行为:递归访问所有子节点"""
for child in self.get_children(node):
self.visit(child)
def get_children(self, node):
"""获取节点的所有子节点"""
for field_val in vars(node).values():
if isinstance(field_val, ASTNode):
yield field_val
elif isinstance(field_val, list):
for item in field_val:
if isinstance(item, ASTNode):
yield item
# 示例:AST 打印访问者
class ASTPrinter(ASTVisitor):
def __init__(self):
self.indent = 0
def _print(self, text):
print(" " * self.indent + text)
def visit_Program(self, node: Program):
self._print("Program")
self.indent += 1
for stmt in node.statements:
self.visit(stmt)
self.indent -= 1
def visit_FuncDecl(self, node: FuncDecl):
self._print(f"FuncDecl: {node.name}() -> {node.return_type}")
self.indent += 1
if node.body:
self.visit(node.body)
self.indent -= 1
def visit_BinaryOp(self, node: BinaryOp):
self._print(f"BinaryOp: {node.op}")
self.indent += 1
self.visit(node.left)
self.visit(node.right)
self.indent -= 1
def visit_IntLiteral(self, node: IntLiteral):
self._print(f"Int: {node.value}")
def visit_Identifier(self, node: Identifier):
self._print(f"Ident: {node.name}")
# 示例:常量折叠访问者
class ConstantFolder(ASTVisitor):
def visit_BinaryOp(self, node: BinaryOp):
# 先递归处理子节点
node.left = self.visit(node.left) or node.left
node.right = self.visit(node.right) or node.right
# 若两个操作数都是常量,则直接计算
if isinstance(node.left, IntLiteral) and isinstance(node.right, IntLiteral):
ops = {'+': lambda a,b: a+b, '-': lambda a,b: a-b,
'*': lambda a,b: a*b, '/': lambda a,b: a//b}
if node.op in ops:
result = ops[node.op](node.left.value, node.right.value)
return IntLiteral(value=result, line=node.line, col=node.col)
return node
5.4 AST 的常见用途
| 用途 | 说明 |
|---|---|
| 类型检查 | 遍历 AST 验证类型一致性 |
| 符号解析 | 将标识符绑定到声明 |
| 代码生成 | 遍历 AST 生成 IR 或目标代码 |
| 静态分析 | 检测潜在 bug、安全漏洞 |
| 代码转换 | 自动重构、代码格式化 |
| 文档生成 | 从 AST 提取注释和类型信息 |
| IDE 支持 | 代码补全、跳转到定义、悬停提示 |
6. 中间表示(IR)
6.1 IR 的作用与设计目标
**中间表示(Intermediate Representation,IR)**是编译器在前端和后端之间使用的一种程序表示形式。
IR 的核心价值:
- 平台无关性:一种 IR 可以对应多个目标平台后端
- 优化便利性:在 IR 层面进行大量目标无关优化
- 多语言支持:多种源语言编译到同一 IR(LLVM 的核心思想)
IR 的层次:
高级 IR(HIR) → 与源语言相近,保留高级结构(循环、数组)
中级 IR(MIR) → 接近三地址码,已分解复杂表达式
低级 IR(LIR) → 接近机器指令,已分配寄存器
6.2 三地址码(Three-Address Code,TAC)
三地址码是最经典的 IR 形式,每条指令最多三个操作数:
x = y op z 二元运算
x = op y 一元运算
x = y 简单赋值
goto L 无条件跳转
if x goto L 条件跳转
x = y[i] 数组读
y[i] = x 数组写
x = &y 取地址
x = *y 解引用读
*x = y 解引用写
param x 参数传递
call f, n 函数调用
x = call f, n 带返回值的调用
return x 返回
源代码到三地址码示例:
c
// 源代码
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
// 三地址码
factorial:
t1 = n <= 1
if t1 goto L_base
t2 = n - 1
t3 = call factorial, 1 // 调用 factorial(t2),参数 1 个
t4 = n * t3
return t4
L_base:
return 1
6.3 静态单赋值形式(SSA)
**静态单赋值(Static Single Assignment,SSA)**是现代编译器 IR 的主流形式,其核心约束是:每个变量只被赋值一次。
SSA 的核心概念
c
// 非 SSA 形式
x = 1
x = x + 2
y = x * 3
// SSA 形式(每次赋值产生新版本)
x_1 = 1
x_2 = x_1 + 2
y_1 = x_2 * 3
ϕ 函数(Phi Function)
当控制流汇合时,SSA 使用 ϕ 函数来选择来自不同前驱块的变量版本:
c
// 原代码
if (cond) {
x = 1;
} else {
x = 2;
}
y = x + 10;
// SSA 形式
if (cond) goto then else goto else_
then:
x_1 = 1
goto merge
else_:
x_2 = 2
goto merge
merge:
x_3 = ϕ(x_1, x_2) // 根据来自哪个前驱块选择值
y_1 = x_3 + 10
SSA 的优势
- Use-Def 链清晰:每个使用点只有一个定义点
- Dead Code Elimination(DCE):没有使用的定义即为死代码
- 常量传播:一个变量若只被赋一个常量,可直接替换
- 公共子表达式消除(CSE):具有相同操作数的指令只需计算一次
- 别名分析简化:无需追踪变量的多次赋值历史
6.4 LLVM IR
LLVM IR 是最著名的现代编译器 IR,被 Clang(C/C++)、Rust、Swift 等语言采用。
LLVM IR 关键特性
- 强类型:每个值都有明确类型
- SSA 形式:所有标量值遵循 SSA
- 无限虚拟寄存器 :用
%name表示,寄存器分配在后端完成 - 显式控制流 :基本块以终止指令结尾(
br、ret、switch) - 三级结构:Module > Function > BasicBlock > Instruction
LLVM IR 完整示例
llvm
; 模块定义
; ModuleID = 'example.c'
target triple = "x86_64-unknown-linux-gnu"
; 全局变量
@global_count = global i32 0, align 4
; 函数定义:int add(int a, int b)
define i32 @add(i32 %a, i32 %b) {
entry:
%result = add i32 %a, %b
ret i32 %result
}
; 函数定义:int factorial(int n)
define i32 @factorial(i32 %n) {
entry:
%cmp = icmp sle i32 %n, 1 ; n <= 1 ?
br i1 %cmp, label %base, label %recurse
base:
ret i32 1
recurse:
%n_minus_1 = sub i32 %n, 1 ; n - 1
%sub_result = call i32 @factorial(i32 %n_minus_1)
%prod = mul i32 %n, %sub_result ; n * factorial(n-1)
ret i32 %prod
}
; 包含循环的函数:int sum(int n)
define i32 @sum(i32 %n) {
entry:
br label %loop
loop:
%i = phi i32 [ 0, %entry ], [ %i_next, %loop ]
%acc = phi i32 [ 0, %entry ], [ %acc_next, %loop ]
%cmp = icmp slt i32 %i, %n
br i1 %cmp, label %body, label %exit
body:
%acc_next = add i32 %acc, %i
%i_next = add i32 %i, 1
br label %loop
exit:
ret i32 %acc
}
LLVM IR 类型系统
llvm
; 整数类型
i1 ; 布尔
i8 ; 字节
i16 ; 短整数
i32 ; 整数
i64 ; 长整数
; 浮点类型
float ; 32位
double ; 64位
; 指针类型
i32* ; 指向 i32 的指针
i8** ; 指向 i8* 的指针
; 数组类型
[4 x i32] ; 4个i32的数组
; 结构类型
{ i32, double, i8* }
; 函数类型
i32 (i32, i32) ; 接受两个i32,返回i32
6.5 控制流图(CFG)
**控制流图(Control Flow Graph,CFG)**是 IR 的图形化表示:
-
节点(Node):基本块(Basic Block)------ 没有内部跳转的直线代码段
-
边(Edge):表示可能的控制流转移(条件/无条件跳转)
entry │ ▼ ┌───────┐ │ cond │ ── n <= 1? └───────┘ / \ 是 否 ▼ ▼ ┌──────┐ ┌─────────┐ │base │ │ recurse │ │ret 1 │ │ n*f(n-1)│ └──────┘ └─────────┘
**基本块(Basic Block)**的性质:
- 只有一个入口点(第一条指令)
- 只有一个出口点(最后一条指令:跳转/返回)
- 若执行该块,则必然从头到尾执行所有指令
6.6 数据流分析
数据流分析是编译优化的理论基础,用于计算程序中各点的静态信息:
| 分析类型 | 方向 | 用途 |
|---|---|---|
| 活跃变量(Liveness) | 后向 | 判断变量在某点后是否还被使用 |
| 到达定义(Reaching Definitions) | 前向 | 某个定义能到达哪些使用点 |
| 可用表达式(Available Expressions) | 前向 | 某个表达式的值是否已被计算 |
| 非常忙表达式(Very Busy Expr) | 后向 | 某表达式在所有路径上都会被用到 |
活跃变量分析(用于寄存器分配):
LiveIn[B] = Use[B] ∪ (LiveOut[B] - Def[B])
LiveOut[B] = ∪ LiveIn[S] (S 为 B 的所有后继块)
Use[B]:在 B 中被使用、且使用前未被定义的变量
Def[B]:在 B 中被定义的变量
7. 编译优化(Optimization)
7.1 优化的分类
按作用范围:
- 局部优化(Local Optimization):在单个基本块内
- 全局优化(Global Optimization):跨基本块,分析整个函数
- 过程间优化(Interprocedural Optimization,IPO):跨函数
- 链接时优化(Link-Time Optimization,LTO):链接阶段优化整个程序
按目标:
- 减少执行时间(Speed Optimization)
- 减少代码体积(Size Optimization)
- 减少内存占用
- 降低能耗
7.2 经典局部优化
常量折叠(Constant Folding)
编译期直接计算常量表达式:
python
# 优化前
x = 2 + 3
y = x * 4
# 若 x 只有一次赋值
# → 常量传播
y = 5 * 4
# → 常量折叠
y = 20
python
def constant_fold(node):
if isinstance(node, BinaryOp):
left = constant_fold(node.left)
right = constant_fold(node.right)
if isinstance(left, IntLiteral) and isinstance(right, IntLiteral):
result = eval_op(node.op, left.value, right.value)
return IntLiteral(value=result)
return node
常量传播(Constant Propagation)
将已知常量的变量替换为常量值:
t1 = 5 t1 = 5
t2 = t1 + 3 → t2 = 5 + 3 → t2 = 8
t3 = t2 * 2 t3 = 8 * 2 t3 = 16
**稀疏条件常量传播(SCCP,Sparse Conditional Constant Propagation)**是 SSA 上的高效常量传播算法,同时处理常量传播和不可达代码消除。
死代码消除(Dead Code Elimination,DCE)
删除其结果从未被使用的指令:
t1 = a + b // t1 从未被使用 → 删除
t2 = c + d // t2 被使用 → 保留
return t2
在 SSA 形式中,DCE 特别高效:若某个 SSA 变量的使用集合为空,则定义它的指令可以删除(前提:指令无副作用)。
公共子表达式消除(CSE,Common Subexpression Elimination)
避免重复计算相同的表达式:
// 优化前
a = b + c * d
e = b + c * d // 相同表达式
// 优化后
t = b + c * d
a = t
e = t
**全局值编号(GVN,Global Value Numbering)**是 SSA 上 CSE 的推广形式,通过哈希表或等价类来识别等价计算。
代数化简(Algebraic Simplification)
利用代数恒等式简化计算:
x + 0 → x (加零恒等)
x * 1 → x (乘一恒等)
x * 0 → 0 (乘零归零)
x - x → 0 (自减归零)
x / x → 1 (自除为一,x ≠ 0)
x * 2 → x << 1 (乘2转位移)
x / 4 → x >> 2 (除以2的幂转位移)
x ** 2 → x * x (消除幂运算)
-(−x) → x (双重取反)
强度削减(Strength Reduction)
用开销更小的操作替换开销大的操作:
// 乘法替换为加法(循环中)
for i in range(n):
a[i * 4] → 使用指针 ptr += 4 来替代乘法
// 除法替换为乘法
x / 3 → x * (1.0/3.0) (浮点情况)
x / 4 → x >> 2 (2的幂情况)
7.3 循环优化
循环是程序中最热点的部分,针对循环的优化效益最大。
循环不变代码外提(LICM,Loop Invariant Code Motion)
将循环不变量移到循环外:
c
// 优化前
for (int i = 0; i < n; i++) {
x = a + b; // a、b 不在循环内改变
arr[i] = i * x;
}
// 优化后
x = a + b; // 外提到循环外
for (int i = 0; i < n; i++) {
arr[i] = i * x;
}
循环展开(Loop Unrolling)
减少循环控制开销,提高指令级并行性:
c
// 原循环
for (int i = 0; i < 8; i++) {
a[i] = b[i] + c[i];
}
// 展开 4 次
for (int i = 0; i < 8; i += 4) {
a[i] = b[i] + c[i];
a[i+1] = b[i+1] + c[i+1];
a[i+2] = b[i+2] + c[i+2];
a[i+3] = b[i+3] + c[i+3];
}
循环向量化(Loop Vectorization / Auto-Vectorization)
将标量循环转换为 SIMD(单指令多数据)操作:
c
// 标量循环
for (int i = 0; i < n; i++) {
c[i] = a[i] + b[i];
}
// 向量化后(伪代码,使用 SIMD)
for (int i = 0; i < n; i += 4) {
c[i:i+4] = a[i:i+4] + b[i:i+4]; // 一条 SIMD 指令处理4个元素
}
LLVM 的向量化 Pass:
LoopVectorize:自动向量化循环SLPVectorize:超字级并行向量化(Superword Level Parallelism)
循环交换(Loop Interchange)
改变嵌套循环的顺序以改善缓存局部性:
c
// 矩阵乘法,内循环访问 B[k][j] 跨步访问(缓存不友好)
for (i) for (j) for (k)
C[i][j] += A[i][k] * B[k][j];
// 交换后,B[k][j] 按行访问(缓存友好)
for (i) for (k) for (j)
C[i][j] += A[i][k] * B[k][j];
其他循环优化
| 优化 | 说明 |
|---|---|
| 循环分裂(Loop Fission) | 将一个循环分拆为多个 |
| 循环融合(Loop Fusion) | 将多个循环合并为一个,减少循环控制开销 |
| 循环分块/Tiling | 将大循环分块以适应 Cache |
| 感应变量消除 | 消除循环中线性变化的变量 |
| 循环旋转(Loop Rotation) | do-while 形式,减少一次条件判断 |
7.4 过程间优化(IPO)
内联(Inlining)
将函数调用替换为函数体,消除调用开销:
c
// 原代码
inline int square(int x) { return x * x; }
int y = square(5);
// 内联后
int y = 5 * 5; // 再经常量折叠 → int y = 25;
内联决策因素:
- 函数体大小(过大不内联,避免代码膨胀)
- 调用频率(热路径优先)
- 参数是否为常量(内联后可进一步折叠)
逃逸分析(Escape Analysis)
分析对象是否会"逃逸"出函数作用域,若不逃逸可以栈分配:
go
// Go 中的逃逸分析
func createObj() *Point {
p := &Point{x: 1, y: 2} // p 逃逸到堆
return p // 因为返回了指针
}
func useObj() {
p := &Point{x: 1, y: 2} // p 不逃逸,栈分配
fmt.Println(p.x + p.y) // 仅在函数内使用
}
7.5 LLVM 优化 Pass 体系
LLVM 使用 Pass(遍) 的概念来组织优化,每个 Pass 读取 IR 并可能修改它。
Analysis Passes(分析 Pass):不修改 IR,只收集信息
Transform Passes(变换 Pass):基于分析结果修改 IR
常用 LLVM Optimization Passes:
mem2reg : 将 alloca 变量提升为 SSA 寄存器(关键 Pass)
instcombine : 指令合并,化简代数表达式
reassociate : 重新结合,使常量计算集中
gvn : 全局值编号,消除冗余计算
sccp : 稀疏条件常量传播
dce : 死代码消除
simplifycfg : 简化控制流图(删除不可达块、合并块等)
licm : 循环不变代码外提
loop-vectorize : 循环向量化
slp-vectorize : SLP 向量化
inline : 函数内联
tailcallelim : 尾调用消除
sroa : 标量聚合替换(Scalar Replacement of Aggregates)
使用 opt 工具运行优化:
bash
# 生成 LLVM IR
clang -emit-llvm -S foo.c -o foo.ll
# 运行特定优化 Pass
opt -passes="mem2reg,instcombine,gvn" foo.ll -o foo_opt.ll
# 运行 O2 优化
opt -O2 foo.ll -o foo_opt.ll
# 查看优化前后差异
opt -passes="instcombine" foo.ll | llvm-dis
7.6 优化级别(Optimization Levels)
| 级别 | GCC/Clang 标志 | 说明 |
|---|---|---|
| O0 | -O0 |
无优化,最快编译,调试友好 |
| O1 | -O1 |
基础优化,不影响编译时间 |
| O2 | -O2 |
推荐生产级别,包含大多数安全优化 |
| O3 | -O3 |
激进优化(内联、向量化),可能增大代码 |
| Os | -Os |
优化代码体积 |
| Oz | -Oz |
最小化代码体积(Clang 特有) |
| Og | -Og |
调试友好的优化 |
8. 代码生成(Code Generation)
8.1 代码生成的主要任务
代码生成是将优化后的 IR 转换为目标机器代码的过程,包括三个核心子任务:
IR
↓
[指令选择 Instruction Selection]
↓
[指令调度 Instruction Scheduling]
↓
[寄存器分配 Register Allocation]
↓
目标代码(汇编/机器码)
8.2 指令选择(Instruction Selection)
指令选择是将 IR 操作映射到目标机器指令的过程。
树模式匹配(Tree Pattern Matching)
将 IR 表示为树,用目标机器指令的"代价模式"来覆盖这棵树:
IR 树:
ADD
/ \
LOAD MUL
| / \
[a] [b] [c]
x86-64 指令覆盖:
mov rax, [b] ; LOAD b
imul rax, [c] ; MUL b, c(结果在 rax)
add rax, [a] ; ADD a + (b*c)
BURG(Bottom-Up Rewrite Grammar) 和 iburg 是常用的树模式匹配工具,使用动态规划找到最低代价的指令覆盖方案。
宏展开(Macro Expansion)
最简单的指令选择方式,每个 IR 操作直接对应一套固定指令:
IR: t1 = a + b
→ mov rax, a ; 加载 a
add rax, b ; 加法
mov t1, rax ; 存储结果
8.3 寄存器分配(Register Allocation)
寄存器分配是将无限的 IR 虚拟寄存器映射到有限的物理寄存器的过程。
图着色寄存器分配(Graph Coloring)
干扰图(Interference Graph):
- 节点 = 变量(活跃区间)
- 边 = 两个变量同时活跃(不能分配同一寄存器)
图着色 = 寄存器分配:用 k 种颜色(k = 寄存器数)对图着色,使相邻节点颜色不同。
算法(Chaitin-Briggs):
1. Build:构建干扰图
2. Simplify:移除度数 < k 的节点,压栈
3. Spill(溢出):若无低度数节点,选一个节点标记为溢出
4. Select:弹栈,为每个节点分配颜色(寄存器)
5. Start Over:若有溢出节点,插入 spill 代码后重新开始
活跃区间(Live Interval)示例:
指令: 1 2 3 4 5 6
变量 a: [===]
变量 b: [=======]
变量 c: [=======]
变量 d: [=======]
a 和 b 互相干扰,b 和 c 互相干扰,c 和 d 互相干扰
若有 2 个寄存器:a→R0, b→R1, c→R0(a已死),d→R1(b已死)
线性扫描寄存器分配(Linear Scan)
比图着色更快(O(n)),广泛用于 JIT 编译器(V8、HotSpot Client):
python
def linear_scan(intervals, num_regs):
active = [] # 当前活跃的区间,按结束点排序
free_regs = list(range(num_regs))
allocation = {}
for interval in sorted(intervals, key=lambda x: x.start):
# 过期的活跃区间归还寄存器
for act in list(active):
if act.end < interval.start:
free_regs.append(allocation[act])
active.remove(act)
active.sort(key=lambda x: x.end)
if not free_regs:
# 溢出处理:溢出结束点最晚的区间
spill = max(active, key=lambda x: x.end)
if spill.end > interval.end:
allocation[interval] = allocation[spill]
allocation[spill] = "spill"
active.remove(spill)
active.append(interval)
else:
allocation[interval] = free_regs.pop(0)
active.append(interval)
active.sort(key=lambda x: x.end)
return allocation
寄存器溢出(Register Spilling)
当寄存器不足时,将部分变量"溢出"到内存(栈):
asm
; 溢出前(理想状态)
; 变量 x 在 rax 中
; 溢出后(x 被溢出到栈)
; 使用 x 时:
mov rax, [rbp - 8] ; 从栈加载 x
add rax, rdx
mov [rbp - 8], rax ; 写回栈
8.4 指令调度(Instruction Scheduling)
指令调度是重排指令顺序,以充分利用 CPU 流水线并减少数据冒险:
延迟槽调度(Delay Slot Scheduling)
RISC 架构中,分支指令后有延迟槽,可以填入有用指令:
asm
; 调度前
beq r1, r2, label ; 分支
nop ; 延迟槽(浪费)
add r3, r4, r5
; 调度后
beq r1, r2, label ; 分支
add r3, r4, r5 ; 填入延迟槽(先执行,无论分支是否跳转)
列表调度(List Scheduling)
基于优先级队列的贪心调度算法:
- 构建依赖图(数据依赖和控制依赖)
- 计算每条指令的"关键路径长度"作为优先级
- 每个时钟周期,从就绪指令中选优先级最高的调度
8.5 目标代码生成示例
以一个简单的 C 函数为例,展示完整的代码生成过程:
c
// 源代码
int sum_array(int *arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
return sum;
}
第一步:LLVM IR
llvm
define i32 @sum_array(i32* %arr, i32 %n) {
entry:
br label %loop
loop:
%i = phi i32 [ 0, %entry ], [ %i_next, %loop ]
%sum = phi i32 [ 0, %entry ], [ %sum_next, %loop ]
%cmp = icmp slt i32 %i, %n
br i1 %cmp, label %body, label %exit
body:
%ptr = getelementptr i32, i32* %arr, i32 %i
%val = load i32, i32* %ptr
%sum_next = add i32 %sum, %val
%i_next = add i32 %i, 1
br label %loop
exit:
ret i32 %sum
}
第二步:x86-64 汇编(优化后)
asm
sum_array:
xorl %eax, %eax ; sum = 0
xorl %ecx, %ecx ; i = 0
testl %esi, %esi ; n == 0?
jle .exit ; 若 n <= 0,跳转到结束
.loop:
addl (%rdi, %rcx, 4), %eax ; sum += arr[i](arr 在 rdi,i 在 rcx)
incl %ecx ; i++
cmpl %esi, %ecx ; i < n?
jl .loop ; 继续循环
.exit:
ret ; 返回值在 eax
8.6 Peephole 优化
**窥孔优化(Peephole Optimization)**在生成的目标代码上局部优化相邻几条指令:
asm
; 优化前
mov rax, rbx ; 将 rbx 复制到 rax
mov rbx, rax ; 再复制回来(冗余!)
; 优化后
mov rax, rbx ; 仅保留有用的一条
; 其他例子
add rax, 0 → (删除,加零无效)
mul rax, 2 → shl rax, 1 (乘2改左移)
9. 工具链与实践教程
9.1 常用编译器工具
| 工具 | 类型 | 说明 |
|---|---|---|
| Flex | 词法分析器生成器 | 基于正则表达式生成 C 词法分析器 |
| Bison / Yacc | 语法分析器生成器 | 基于 CFG 生成 LALR(1) 解析器 |
| ANTLR4 | 解析器生成器 | 支持多目标语言,生成 LL(*) 解析器 |
| LLVM | 编译器基础设施 | IR、优化、代码生成,支持多目标 |
| GCC | 完整编译器 | GNU 编译器集合 |
| Clang | C/C++ 前端 | 基于 LLVM,错误信息友好 |
| tree-sitter | 语法分析库 | 用于 IDE 的增量解析 |
9.2 ANTLR4 教程
ANTLR4 是目前最流行的解析器生成器,支持生成 Java、Python、C++、Go 等多种目标语言的解析器。
安装:
bash
pip install antlr4-tools
# 或
brew install antlr
编写文法文件(Expr.g4):
antlr
grammar Expr;
// 解析规则
prog : stat+ ;
stat : expr NEWLINE
| ID '=' expr NEWLINE
| NEWLINE
;
expr : expr ('*'|'/') expr # MulDiv
| expr ('+'|'-') expr # AddSub
| INT # int
| ID # id
| '(' expr ')' # parens
;
// 词法规则
MulDiv : [*/] ;
AddDiv : [+-] ;
INT : [0-9]+ ;
ID : [a-zA-Z]+ ;
NEWLINE : '\r'? '\n' ;
WS : [ \t]+ -> skip ;
生成解析器:
bash
antlr4 -Dlanguage=Python3 Expr.g4
Python 实现访问者:
python
from antlr4 import *
from ExprLexer import ExprLexer
from ExprParser import ExprParser
from ExprVisitor import ExprVisitor
class EvalVisitor(ExprVisitor):
def visitMulDiv(self, ctx):
left = self.visit(ctx.expr(0))
right = self.visit(ctx.expr(1))
if ctx.op.type == ExprParser.MUL:
return left * right
return left / right
def visitAddSub(self, ctx):
left = self.visit(ctx.expr(0))
right = self.visit(ctx.expr(1))
if ctx.op.type == ExprParser.ADD:
return left + right
return left - right
def visitInt(self, ctx):
return int(ctx.INT().getText())
# 运行
input_stream = InputStream("3 + 4 * 2\n")
lexer = ExprLexer(input_stream)
stream = CommonTokenStream(lexer)
parser = ExprParser(stream)
tree = parser.prog()
visitor = EvalVisitor()
print(visitor.visit(tree)) # 输出: 11
9.3 使用 LLVM Python 绑定(llvmlite)
python
from llvmlite import ir, binding
# 初始化 LLVM
binding.initialize()
binding.initialize_native_target()
binding.initialize_native_asmprinter()
# 创建模块和函数
module = ir.Module(name='example')
func_type = ir.FunctionType(ir.IntType(32), [ir.IntType(32), ir.IntType(32)])
func = ir.Function(module, func_type, name='add')
a, b = func.args
a.name, b.name = 'a', 'b'
# 构建基本块
block = func.append_basic_block(name='entry')
builder = ir.IRBuilder(block)
# 生成加法指令
result = builder.add(a, b, name='result')
builder.ret(result)
print(module) # 输出 LLVM IR
# 编译到机器码
target = binding.Target.from_default_triple()
target_machine = target.create_target_machine()
mod = binding.parse_assembly(str(module))
mod.verify()
print(target_machine.emit_assembly(mod)) # 输出汇编代码
9.4 使用 Clang 查看 LLVM IR
bash
# 生成 LLVM IR(文本格式)
clang -S -emit-llvm -O0 foo.c -o foo.ll
# 生成 LLVM IR(优化后)
clang -S -emit-llvm -O2 foo.c -o foo_opt.ll
# 运行优化 Pass
opt -S -passes="mem2reg,instcombine,gvn,simplifycfg" foo.ll -o foo_opt.ll
# 查看优化统计
opt -S -O2 --stats foo.ll -o foo_opt.ll 2>&1
# 查看 CFG(生成 dot 文件)
opt -passes="dot-cfg" foo.ll
dot -Tpng .add.dot -o cfg.png
# 生成 x86 汇编
llc foo_opt.ll -o foo.s
# 生成目标文件并链接
clang foo.s -o foo
# 查看 AST(Clang)
clang -Xclang -ast-dump foo.c
10. 实战示例:从零构建一个简单编译器
下面用 Python 实现一个完整的迷你编译器,支持以下特性:
- 变量声明和赋值
- 算术表达式
- if/else 语句
- while 循环
- 函数调用(print)
- 目标:生成三地址码
10.1 完整项目结构
mini_compiler/
├── lexer.py # 词法分析器
├── parser.py # 语法分析器
├── ast_nodes.py # AST 节点定义
├── semantic.py # 语义分析
├── codegen.py # 代码生成(三地址码)
├── optimizer.py # 简单优化
└── main.py # 主程序
10.2 词法分析器(lexer.py)
python
import re
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Token:
type: str
value: str
line: int
col: int
def __repr__(self):
return f'Token({self.type}, {self.value!r}, {self.line}:{self.col})'
KEYWORDS = {'if', 'else', 'while', 'int', 'float', 'return', 'print'}
TOKEN_PATTERNS = [
('FLOAT', r'\d+\.\d*'),
('INT', r'\d+'),
('ID', r'[a-zA-Z_]\w*'),
('EQ', r'=='),
('NE', r'!='),
('LE', r'<='),
('GE', r'>='),
('ASSIGN', r'='),
('PLUS', r'\+'),
('MINUS', r'-'),
('STAR', r'\*'),
('SLASH', r'/'),
('LT', r'<'),
('GT', r'>'),
('AND', r'&&'),
('OR', r'\|\|'),
('NOT', r'!'),
('SEMI', r';'),
('COMMA', r','),
('LPAREN', r'\('),
('RPAREN', r'\)'),
('LBRACE', r'\{'),
('RBRACE', r'\}'),
('NEWLINE', r'\n'),
('SKIP', r'[ \t\r]+|//[^\n]*'), # 空白和注释
]
MASTER_RE = re.compile(
'|'.join(f'(?P<{name}>{pattern})' for name, pattern in TOKEN_PATTERNS)
)
def tokenize(source: str) -> List[Token]:
tokens = []
line = 1
line_start = 0
for mo in MASTER_RE.finditer(source):
kind = mo.lastgroup
value = mo.group()
col = mo.start() - line_start + 1
if kind == 'NEWLINE':
line += 1
line_start = mo.end()
continue
elif kind == 'SKIP':
continue
elif kind == 'ID' and value in KEYWORDS:
kind = value.upper() # 将关键字转换为相应类型
tokens.append(Token(kind, value, line, col))
tokens.append(Token('EOF', '', line, 0))
return tokens
10.3 语法分析器(parser.py)
python
from ast_nodes import *
from lexer import Token
from typing import List
class ParseError(Exception):
pass
class Parser:
def __init__(self, tokens: List[Token]):
self.tokens = tokens
self.pos = 0
def current(self) -> Token:
return self.tokens[self.pos]
def peek(self, offset=1) -> Token:
idx = self.pos + offset
return self.tokens[idx] if idx < len(self.tokens) else self.tokens[-1]
def consume(self, expected_type: str = None) -> Token:
tok = self.current()
if expected_type and tok.type != expected_type:
raise ParseError(
f"第 {tok.line} 行:期望 {expected_type},得到 {tok.type}({tok.value!r})")
self.pos += 1
return tok
def match(self, *types) -> bool:
return self.current().type in types
# ─── 程序 ──────────────────────────────────────────
def parse_program(self) -> Program:
stmts = []
while not self.match('EOF'):
stmts.append(self.parse_statement())
return Program(statements=stmts)
# ─── 语句 ──────────────────────────────────────────
def parse_statement(self) -> ASTNode:
if self.match('INT', 'FLOAT'):
return self.parse_var_decl()
elif self.match('IF'):
return self.parse_if()
elif self.match('WHILE'):
return self.parse_while()
elif self.match('RETURN'):
return self.parse_return()
elif self.match('LBRACE'):
return self.parse_block()
elif self.match('PRINT'):
return self.parse_print()
else:
return self.parse_expr_stmt()
def parse_var_decl(self) -> VarDecl:
type_tok = self.consume()
name_tok = self.consume('ID')
init = None
if self.match('ASSIGN'):
self.consume('ASSIGN')
init = self.parse_expr()
self.consume('SEMI')
return VarDecl(type_name=type_tok.value, name=name_tok.value,
init=init, line=type_tok.line)
def parse_if(self) -> IfStmt:
tok = self.consume('IF')
self.consume('LPAREN')
cond = self.parse_expr()
self.consume('RPAREN')
then_block = self.parse_block()
else_block = None
if self.match('ELSE'):
self.consume('ELSE')
else_block = self.parse_block()
return IfStmt(condition=cond, then_block=then_block,
else_block=else_block, line=tok.line)
def parse_while(self) -> WhileStmt:
tok = self.consume('WHILE')
self.consume('LPAREN')
cond = self.parse_expr()
self.consume('RPAREN')
body = self.parse_block()
return WhileStmt(condition=cond, body=body, line=tok.line)
def parse_return(self) -> ReturnStmt:
tok = self.consume('RETURN')
val = None
if not self.match('SEMI'):
val = self.parse_expr()
self.consume('SEMI')
return ReturnStmt(value=val, line=tok.line)
def parse_block(self) -> Block:
self.consume('LBRACE')
stmts = []
while not self.match('RBRACE', 'EOF'):
stmts.append(self.parse_statement())
self.consume('RBRACE')
return Block(statements=stmts)
def parse_print(self) -> CallExpr:
tok = self.consume('PRINT')
self.consume('LPAREN')
args = [self.parse_expr()]
self.consume('RPAREN')
self.consume('SEMI')
return CallExpr(callee='print', args=args, line=tok.line)
def parse_expr_stmt(self) -> ASTNode:
expr = self.parse_expr()
self.consume('SEMI')
return expr
# ─── 表达式(优先级爬升法)──────────────────────────
def parse_expr(self) -> ASTNode:
return self.parse_assignment()
def parse_assignment(self) -> ASTNode:
left = self.parse_or()
if self.match('ASSIGN') and isinstance(left, Identifier):
tok = self.consume('ASSIGN')
right = self.parse_assignment()
return AssignStmt(target=left, value=right, line=tok.line)
return left
def parse_or(self) -> ASTNode:
left = self.parse_and()
while self.match('OR'):
op = self.consume().value
right = self.parse_and()
left = BinaryOp(op=op, left=left, right=right)
return left
def parse_and(self) -> ASTNode:
left = self.parse_comparison()
while self.match('AND'):
op = self.consume().value
right = self.parse_comparison()
left = BinaryOp(op=op, left=left, right=right)
return left
def parse_comparison(self) -> ASTNode:
left = self.parse_addition()
while self.match('EQ', 'NE', 'LT', 'GT', 'LE', 'GE'):
op = self.consume().value
right = self.parse_addition()
left = BinaryOp(op=op, left=left, right=right)
return left
def parse_addition(self) -> ASTNode:
left = self.parse_multiplication()
while self.match('PLUS', 'MINUS'):
op = self.consume().value
right = self.parse_multiplication()
left = BinaryOp(op=op, left=left, right=right)
return left
def parse_multiplication(self) -> ASTNode:
left = self.parse_unary()
while self.match('STAR', 'SLASH'):
op = self.consume().value
right = self.parse_unary()
left = BinaryOp(op=op, left=left, right=right)
return left
def parse_unary(self) -> ASTNode:
if self.match('MINUS', 'NOT'):
op = self.consume().value
operand = self.parse_unary()
return UnaryOp(op=op, operand=operand)
return self.parse_primary()
def parse_primary(self) -> ASTNode:
tok = self.current()
if tok.type == 'INT':
self.consume()
return IntLiteral(value=int(tok.value), line=tok.line)
elif tok.type == 'FLOAT':
self.consume()
return FloatLiteral(value=float(tok.value), line=tok.line)
elif tok.type == 'ID':
self.consume()
if self.match('LPAREN'):
return self.parse_call(tok)
return Identifier(name=tok.value, line=tok.line)
elif tok.type == 'LPAREN':
self.consume('LPAREN')
expr = self.parse_expr()
self.consume('RPAREN')
return expr
raise ParseError(f"第 {tok.line} 行:意外的 Token {tok.type}({tok.value!r})")
def parse_call(self, name_tok: Token) -> CallExpr:
self.consume('LPAREN')
args = []
if not self.match('RPAREN'):
args.append(self.parse_expr())
while self.match('COMMA'):
self.consume('COMMA')
args.append(self.parse_expr())
self.consume('RPAREN')
return CallExpr(callee=name_tok.value, args=args, line=name_tok.line)
10.4 代码生成(codegen.py,生成三地址码)
python
from ast_nodes import *
class CodeGenerator:
def __init__(self):
self.instructions = []
self.temp_count = 0
self.label_count = 0
def new_temp(self) -> str:
self.temp_count += 1
return f't{self.temp_count}'
def new_label(self) -> str:
self.label_count += 1
return f'L{self.label_count}'
def emit(self, instr: str):
self.instructions.append(instr)
def emit_label(self, label: str):
self.instructions.append(f'{label}:')
def generate(self, node: ASTNode) -> str:
method = f'gen_{type(node).__name__}'
return getattr(self, method, self.gen_default)(node)
def gen_default(self, node):
raise NotImplementedError(f"未实现: {type(node).__name__}")
def gen_Program(self, node: Program) -> str:
for stmt in node.statements:
self.generate(stmt)
return '\n'.join(self.instructions)
def gen_VarDecl(self, node: VarDecl):
if node.init:
val = self.generate(node.init)
self.emit(f'{node.name} = {val}')
def gen_AssignStmt(self, node: AssignStmt):
val = self.generate(node.value)
self.emit(f'{node.target.name} = {val}')
def gen_BinaryOp(self, node: BinaryOp) -> str:
left = self.generate(node.left)
right = self.generate(node.right)
t = self.new_temp()
self.emit(f'{t} = {left} {node.op} {right}')
return t
def gen_UnaryOp(self, node: UnaryOp) -> str:
operand = self.generate(node.operand)
t = self.new_temp()
self.emit(f'{t} = {node.op}{operand}')
return t
def gen_IfStmt(self, node: IfStmt):
cond = self.generate(node.condition)
else_label = self.new_label()
end_label = self.new_label()
self.emit(f'ifFalse {cond} goto {else_label}')
self.generate(node.then_block)
if node.else_block:
self.emit(f'goto {end_label}')
self.emit_label(else_label)
if node.else_block:
self.generate(node.else_block)
self.emit_label(end_label)
def gen_WhileStmt(self, node: WhileStmt):
start_label = self.new_label()
end_label = self.new_label()
self.emit_label(start_label)
cond = self.generate(node.condition)
self.emit(f'ifFalse {cond} goto {end_label}')
self.generate(node.body)
self.emit(f'goto {start_label}')
self.emit_label(end_label)
def gen_Block(self, node: Block):
for stmt in node.statements:
self.generate(stmt)
def gen_ReturnStmt(self, node: ReturnStmt):
if node.value:
val = self.generate(node.value)
self.emit(f'return {val}')
else:
self.emit('return')
def gen_CallExpr(self, node: CallExpr) -> str:
arg_vals = [self.generate(arg) for arg in node.args]
for arg in arg_vals:
self.emit(f'param {arg}')
if node.callee == 'print':
self.emit(f'call print, {len(arg_vals)}')
return ''
t = self.new_temp()
self.emit(f'{t} = call {node.callee}, {len(arg_vals)}')
return t
def gen_Identifier(self, node: Identifier) -> str:
return node.name
def gen_IntLiteral(self, node: IntLiteral) -> str:
return str(node.value)
def gen_FloatLiteral(self, node: FloatLiteral) -> str:
return str(node.value)
10.5 主程序(main.py)
python
from lexer import tokenize
from parser import Parser
from codegen import CodeGenerator
def compile_code(source: str) -> str:
print("=== 源代码 ===")
print(source)
print("\n=== 词法分析(Tokens)===")
tokens = tokenize(source)
for tok in tokens[:-1]: # 跳过 EOF
print(f" {tok}")
print("\n=== 语法分析(AST)===")
parser = Parser(tokens)
ast = parser.parse_program()
print(ast)
print("\n=== 生成三地址码 ===")
codegen = CodeGenerator()
tac = codegen.generate(ast)
print(tac)
return tac
# 测试
source = """
int x = 10;
int y = 20;
int z = x + y * 2;
if (z > 30) {
print(z);
} else {
print(0);
}
int i = 0;
while (i < 5) {
print(i);
i = i + 1;
}
"""
compile_code(source)
输出结果(三地址码部分):
x = 10
y = 20
t1 = y * 2
t2 = x + t1
z = t2
t3 = z > 30
ifFalse t3 goto L1
param z
call print, 1
goto L2
L1:
param 0
call print, 1
L2:
i = 0
L3:
t4 = i < 5
ifFalse t4 goto L4
param i
call print, 1
t5 = i + 1
i = t5
goto L3
L4:
11. 现代编译器技术前沿
11.1 JIT 编译(Just-In-Time Compilation)
JIT 编译在程序运行时将字节码或 IR 编译为机器码,结合了解释执行的灵活性和编译执行的高性能。
典型 JIT 系统:
- Java HotSpot:先用解释器执行,对热点方法进行 JIT 编译
- V8(Node.js):Ignition 解释器 + Turbofan JIT 编译器
- PyPy:Python 的 JIT 实现,针对追踪(Tracing)JIT
- LLVM ORC JIT:基于 LLVM 的在线 JIT 框架
分层编译(Tiered Compilation):
字节码
↓
[第1层:解释执行] ← 快速启动
↓ 热点检测
[第2层:基线 JIT] ← 快速编译,中等优化
↓ 更热
[第3层:优化 JIT] ← 慢速编译,深度优化
11.2 增量编译与语言服务器(LSP)
现代 IDE 需要编译器支持增量解析 和实时语义分析:
- tree-sitter:增量解析库,支持在文本修改后只重新解析受影响的部分
- LSP(Language Server Protocol):微软提出的标准协议,将编译器的语义能力(补全、诊断、跳转)暴露给 IDE
11.3 MLIR(多级中间表示)
MLIR(Multi-Level Intermediate Representation)是 LLVM 项目孵化的下一代编译器基础设施,特别为机器学习编译器设计。
核心特性:
-
方言(Dialect):允许在同一 IR 中混合不同抽象层次
-
渐进式降低(Progressive Lowering):从高级方言逐步降到低级
-
可扩展性:用户可以定义自己的操作和类型
TensorFlow Graph Dialect
↓ lower
HLO (High Level Operations) Dialect
↓ lower
Linalg Dialect (线性代数)
↓ lower
Affine Dialect (仿射循环)
↓ lower
SCF (Structured Control Flow) Dialect
↓ lower
Standard Dialect
↓ lower
LLVM IR Dialect
↓
机器码
11.4 WebAssembly(WASM)
WebAssembly 是一种二进制指令格式,作为 Web 平台的低级虚拟机:
- Rust、C/C++、Go 等语言可编译到 WASM
- WASI(WebAssembly System Interface):使 WASM 可以在浏览器外运行
- 组件模型:支持跨语言模块互操作
bash
# Rust 编译到 WASM
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown
# C 编译到 WASM(使用 Emscripten)
emcc foo.c -o foo.html
11.5 AI 辅助编译优化
现代研究将机器学习引入编译器优化:
- 自动调优(Auto-Tuning):使用强化学习搜索最优编译 Pass 序列(如 AutoPhase)
- 神经网络指令调度:用 GNN 预测最优指令排列
- 向量化预测:预测哪些循环值得向量化
- 内联决策:用 ML 模型替代启发式内联策略
12. 参考资源
经典教材
| 书名 | 作者 | 说明 |
|---|---|---|
| 龙书 Compilers: Principles, Techniques, and Tools | Aho, Lam, Sethi, Ullman | 编译原理圣经,必读 |
| 虎书 Modern Compiler Implementation in Java/C/ML | Appel | 更注重实践,代码示例完整 |
| 鲸书 Advanced Compiler Design and Implementation | Muchnick | 深入优化技术,参考书 |
| Engineering a Compiler | Cooper & Torczon | 现代教材,讲解清晰 |
| Crafting Interpreters | Robert Nystrom | 免费在线,从零实现语言 |
在线资源
- LLVM 官方文档:https://llvm.org/docs/
- LLVM IR 语言参考:https://llvm.org/docs/LangRef.html
- LLVM Passes 文档:https://llvm.org/docs/Passes.html
- ANTLR4 文档:https://github.com/antlr/antlr4/blob/master/doc/index.md
- Crafting Interpreters(免费在线书):https://craftinginterpreters.com/
- CS143 Stanford 编译原理:https://web.stanford.edu/class/cs143/
- MLIR 官方文档:https://mlir.llvm.org/
实践项目推荐
- 手写 Lisp 解释器:最小化,但涵盖所有核心概念
- 实现一个子集 C 编译器:理解完整编译流程
- 写一个 Brainfuck 解释器/编译器:词法、解析、执行全掌握
- 为现有语言写 LLVM 前端:深度理解 LLVM IR
- 给 LLVM 写一个优化 Pass:深入了解 SSA 和数据流分析