编译原理:从理论到实战全解析

编译原理完整知识总结与使用教程


目录

  1. 编译器概述与整体架构
  2. [词法分析(Lexical Analysis)](#词法分析(Lexical Analysis))
  3. [语法分析(Syntax Analysis / Parsing)](#语法分析(Syntax Analysis / Parsing))
  4. [语义分析(Semantic Analysis)](#语义分析(Semantic Analysis))
  5. 抽象语法树(AST)
  6. 中间表示(IR)
  7. 编译优化(Optimization)
  8. [代码生成(Code Generation)](#代码生成(Code Generation))
  9. 工具链与实践教程
  10. 实战示例:从零构建一个简单编译器
  11. 现代编译器技术前沿
  12. 参考资源

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) 源代码中匹配某个模式的字符序列(如 intx=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 最长匹配原则与优先规则

词法分析器遵循以下两条核心原则:

  1. 最长匹配(Maximal Munch):在多个候选 Token 中,选择能匹配最长字符串的那个。

    • 例如:<= 应被识别为 TOKEN_LE,而不是 TOKEN_LT 后跟 TOKEN_ASSIGN
  2. 优先规则(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),如 ExprStmt
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 错误处理与恢复

语法分析器必须能够处理错误并尽量继续解析,以发现更多错误:

错误恢复策略:

  1. 紧急模式(Panic Mode) :丢弃输入 Token 直到找到一个可以继续解析的同步符号(如 ;}
  2. 短语级别恢复(Phrase-Level Recovery):对当前处理的短语进行局部修正(插入/删除 Token)
  3. 错误产生式(Error Productions):在文法中加入常见错误的产生式
  4. 全局纠错(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 的优势
  1. Use-Def 链清晰:每个使用点只有一个定义点
  2. Dead Code Elimination(DCE):没有使用的定义即为死代码
  3. 常量传播:一个变量若只被赋一个常量,可直接替换
  4. 公共子表达式消除(CSE):具有相同操作数的指令只需计算一次
  5. 别名分析简化:无需追踪变量的多次赋值历史

6.4 LLVM IR

LLVM IR 是最著名的现代编译器 IR,被 Clang(C/C++)、Rust、Swift 等语言采用。

LLVM IR 关键特性
  • 强类型:每个值都有明确类型
  • SSA 形式:所有标量值遵循 SSA
  • 无限虚拟寄存器 :用 %name 表示,寄存器分配在后端完成
  • 显式控制流 :基本块以终止指令结尾(brretswitch
  • 三级结构: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)**的性质:

  1. 只有一个入口点(第一条指令)
  2. 只有一个出口点(最后一条指令:跳转/返回)
  3. 若执行该块,则必然从头到尾执行所有指令

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)

基于优先级队列的贪心调度算法:

  1. 构建依赖图(数据依赖和控制依赖)
  2. 计算每条指令的"关键路径长度"作为优先级
  3. 每个时钟周期,从就绪指令中选优先级最高的调度

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 免费在线,从零实现语言

在线资源

实践项目推荐

  1. 手写 Lisp 解释器:最小化,但涵盖所有核心概念
  2. 实现一个子集 C 编译器:理解完整编译流程
  3. 写一个 Brainfuck 解释器/编译器:词法、解析、执行全掌握
  4. 为现有语言写 LLVM 前端:深度理解 LLVM IR
  5. 给 LLVM 写一个优化 Pass:深入了解 SSA 和数据流分析
相关推荐
xuhaoyu_cpp_java2 小时前
Maven学习(一)
java·经验分享·笔记·学习·maven
Qt程序员2 小时前
Linux 内核 SPI 驱动
linux·linux内核·嵌入式开发·spi
sibylyue2 小时前
Nginx\Tomcat\Jetty\Netty
java·nginx·http
于先生吖2 小时前
基于 SpringBoot 架构,高性能 JAVA 动漫短剧系统源码
java·开发语言·spring boot
chen_ever2 小时前
从网络基础到吃透 Linux 高并发 I/O 核心(epoll+零拷贝 完整版)
linux·网络·c++·后端
斌味代码2 小时前
SpringBoot 3 实战:虚拟线程、全局异常处理与 JWT 鉴权完整方案
java·spring boot·后端
木下~learning2 小时前
零基础Git入门:Linux+Gitee实战指南
linux·git·gitee·github·虚拟机·版本控制·ubunt
程序设计实验室2 小时前
Python网络请求库,从 requests 到 httpx
python
电商API&Tina2 小时前
跨境电商如何接入1688官方寻源通接口?附接入流程
java·数据库·python·sql·oracle·json·php