云计算百科
云计算领域专业知识百科平台

手写编译器:从词法到代码生成的技术深度解析

手写编译器(Handwritten Compiler)指不依赖 Lex/Yacc、ANTLR 等生成工具,完全由程序员使用通用编程语言(如 C++、Rust、Go)实现编译器前端(Frontend)乃至后端(Backend)的全过程。其核心价值在于细粒度控制、极致优化、教学研究、DSL 高度定制化。


1. 词法分析(Lexical Analysis)

目标

将字符流(char* 或 std::string_view)转换为标记流(Token Stream),每个 Token 包含:

  • 类型(TokenType,如 TK_IDENTIFIER, TK_PLUS, KW_IF)
  • 原始文本(std::string_view,避免拷贝)
  • 位置信息(Location:行、列、文件 ID)

实现技术:确定性有限自动机(DFA)驱动

enum TokenType {
TK_EOF,
TK_IDENTIFIER,
TK_NUMBER,
TK_PLUS, TK_MINUS, /* … */
KW_IF, KW_WHILE, /* … */
};

struct Token {
TokenType type;
std::string_view text;
Location loc;
};

class Lexer {
const char* input; // 输入指针
const char* ptr; // 当前扫描位置
int line, column;
Token next_token; // 预读 Token(用于 lookahead)
bool has_next;

public:
Lexer(const char* input) : input(input), ptr(input), line(1), column(1), has_next(false) {}

Token next(); // 主接口
Token scan(); // 核心扫描逻辑
void skip_whitespace(); // 跳过空白与注释
};

扫描核心:状态机 + 查表优化

Token Lexer::scan() {
skip_whitespace();
const char* start = ptr;

switch (*ptr) {
case '\\0': return {TK_EOF, "", {line, column}};
case '+': advance(); return {TK_PLUS, "+", loc(start)};
case '-': advance(); return {TK_MINUS, "-", loc(start)};
case '0'...'9': return scan_number(start);
case 'a'...'z':
case 'A'...'Z':
case '_': return scan_identifier(start);
// … 其他字符
default:
error("Unexpected character: ", *ptr);
}
}

关键优化点:
  • 零拷贝:使用 std::string_view 指向源码子串,避免频繁 std::string 构造。
  • 关键字哈希表:标识符识别后,查静态哈希表判断是否为关键字。static const std::unordered_map<std::string_view, TokenType> keyword_map = {
    {"if", KW_IF}, {"while", KW_WHILE}, /* … */
    };

  • DFA 表驱动(高级):将状态转移编码为二维表,用 state = table[state][char_class] 加速。

2. 语法分析(Syntax Analysis)

主流选择:递归下降解析(Recursive Descent Parsing, RDP)

适用于 LL(k) 文法,手写首选,因其:

  • 控制流清晰
  • 易于插入语义动作(如 AST 构造)
  • 错误恢复灵活

消除左递归(Left Recursion Elimination)

原始表达式文法(左递归):

Expr → Expr '+' Term | Term

转换为右递归:

Expr → Term Expr'
Expr' → '+' Term Expr' | ε

对应 C++ 实现:

std::unique_ptr<ExprAST> parse_expr() {
auto LHS = parse_term();
return parse_expr_rhs(0, std::move(LHS));
}

std::unique_ptr<ExprAST> parse_expr_rhs(int min_precedence, std::unique_ptr<ExprAST> LHS) {
while (true) {
int tok_prec = get precedence(peek().type);
if (tok_prec < min_precedence) break;

Token op = consume(); // consume '+'/'-'
auto RHS = parse_term();

int next_prec = get_precedence(peek().type);
if (tok_prec < next_prec) {
RHS = parse_expr_rhs(tok_prec + 1, std::move(RHS));
}

LHS = std::make_unique<BinaryExprAST>(std::move(LHS), op, std::move(RHS));
}
return LHS;
}

技巧:使用“优先级驱动解析”(Pratt Parsing)高效处理表达式,时间复杂度 O(n)。


3. 抽象语法树(AST)设计

设计原则

  • 不可变性(Immutable):AST 节点构造后不应修改。
  • 多态性:使用虚函数或标签联合(Tagged Union)。
  • 位置信息嵌入:每个节点携带 Location 用于错误报告。

struct Location { int line, col; };

class ASTNode {
public:
virtual ~ASTNode() = default;
virtual void accept(ASTVisitor* v) = 0;
Location loc;
};

class ExprAST : public ASTNode { /* … */ };
class BinaryExprAST : public ExprAST {
std::unique_ptr<ExprAST> lhs, rhs;
Token op;
public:
void accept(ASTVisitor* v) override { v->visit(*this); }
};


4. 语义分析(Semantic Analysis)

核心:符号表(Symbol Table)与类型系统

符号表结构:作用域栈(Scope Stack)

struct Symbol {
std::string name;
Type* type;
StorageKind storage; // local, global, param
int offset; // frame offset
};

class Scope {
std::unordered_map<std::string, std::unique_ptr<Symbol>> symbols;
Scope* parent;
public:
void insert(const std::string& name, std::unique_ptr<Symbol> sym);
Symbol* lookup(const std::string& name); // 向上查找
};

class SemanticAnalyzer : public ASTVisitor {
std::stack<std::unique_ptr<Scope>> scope_stack;
Type* current_function_return_type;

public:
void visit(BinaryExprAST& expr) override;
void visit(VarDeclAST& decl) override;
void visit(IdentifierExprAST& id) override;
// …
};

类型检查算法(以 BinaryExpr 为例)

void SemanticAnalyzer::visit(BinaryExprAST& expr) {
expr.lhs->accept(*this);
expr.rhs->accept(*this);

Type* lhs_type = expr.lhs->type;
Type* rhs_type = expr.rhs->type;

if (expr.op.type == TK_PLUS) {
if (lhs_type->is_numeric() && rhs_type->is_numeric()) {
expr.type = unify_numeric(lhs_type, rhs_type); // 类型提升
} else if (lhs_type->is_pointer() && rhs_type->is_integral()) {
expr.type = lhs_type; // 指针算术
} else {
error("Invalid operand types for +", expr.loc);
}
}
// … 其他操作符
}


5. 中间表示(Intermediate Representation, IR)

选择:三地址码(Three-Address Code, TAC)或 SSA 形式

TAC 示例:

// a = b + c * d
t1 = c * d
t2 = b + t1
a = t2

实现 IR 指令:

enum Opcode {
OP_ADD, OP_SUB, OP_MUL, OP_DIV,
OP_LOAD, OP_STORE, OP_CALL,
OP_JMP, OP_JZ, OP_RET
};

struct IRInstruction {
Opcode op;
Operand dest, src1, src2;
std::optional<Label> label;
};

从 AST 生成 TAC(使用 Visitor 模式)

class IRGenerator : public ASTVisitor {
std::vector<std::unique_ptr<IRInstruction>> instructions;
int temp_counter = 0;

Temp new_temp() { return Temp{temp_counter++}; }

Temp visit(BinaryExprAST& expr) override {
Temp lhs = expr.lhs->accept(*this);
Temp rhs = expr.rhs->accept(*this);
Temp result = new_temp();

Opcode op = get_opcode(expr.op.type);
instructions.push_back(std::make_unique<IRInst>(op, result, lhs, rhs));

return result;
}
};


6. 代码生成(Code Generation)

目标:x86-64 汇编(AT&T 语法)

寄存器分配:线性扫描(Linear Scan)或图着色(Graph Coloring)

以线性扫描为例:

  • 计算每个变量的活跃区间(Live Range)
  • 维护活跃变量集合,按区间起始排序
  • 分配物理寄存器或溢出到栈
  • 指令选择:模式匹配 + 模板

    void CodeGenerator::visit(BinaryExprAST& expr) {
    Reg lhs = expr.lhs->accept(*this);
    Reg rhs = expr.rhs->accept(*this);
    Reg result = alloc_reg(); // 分配寄存器

    switch (expr.op.type) {
    case TK_PLUS:
    emit("addq %s, %s", reg_name(rhs), reg_name(result));
    break;
    case TK_MUL:
    emit("imulq %s, %s", reg_name(rhs), reg_name(result));
    break;
    }
    }

    调用约定(x86-64 System V ABI)
    • 前 6 个整数参数:%rdi, %rsi, %rdx, %rcx, %r8, %r9
    • 返回值:%rax
    • 被调用者保存:%rbx, %rbp, %r12-%r15

    7. 优化技术(手写实现)

    常见优化 Pass(遍历 IR 或 AST)

    优化描述实现技术
    常量折叠 3 + 4 → 7 在 IR 生成时进行常量传播
    死代码消除 移除不可达基本块 构建 CFG,进行可达性分析
    公共子表达式消除 a=b+c; d=b+c → t=b+c; a=t; d=t 哈希表达式,查找重复计算
    循环不变量外提 将循环内不变计算移出 检测循环内表达式是否依赖循环变量

    8. 错误处理与诊断

    精准错误报告

    • 使用 Location 定位源码行。
    • 提供上下文(如打印出错行)。
    • 支持多错误收集(不因一个错误终止)。

    class Diagnostics {
    std::vector<Diagnostic> errors;
    public:
    void error(const std::string& msg, Location loc) {
    errors.push_back({ERROR, msg, loc});
    print_source_context(loc);
    }
    };


    9. 性能考量

    • 内存管理:使用对象池(Object Pool)或 Arena Allocator 减少 new/delete 开销。
    • 字符串处理:string_view + 字符串驻留(String Interning)避免重复。
    • 缓存友好:IR 指令连续存储,符号表使用哈希表。
    • 并行编译:现代编译器支持多文件并行编译(需独立上下文)。

    10. 工业级实践参考

    • Clang:C++ 前端,手写递归下降解析器 + AST。
    • Rustc:使用 librustc_parse 手写 LL(1) 解析器。
    • V8 Ignition:JavaScript 字节码生成器手写。
    • LuaJIT:高度优化的手写汇编生成。

    总结:手写 vs 工具生成

    维度手写编译器工具生成(Yacc/ANTLR)
    性能 高(可优化) 中等
    开发速度
    调试难度 高(需理解状态机) 中等(语法冲突难调)
    灵活性 极高(可嵌入语义) 有限
    适用场景 教学、DSL、极致优化 通用语言、快速原型

    结论:手写编译器是系统编程的“黑带”技能。它要求对形式语言、自动机、数据流分析、目标架构有深刻理解。

    建议路径:从实现一个支持 int、+ – * /、if、while、函数调用的子集语言编译器开始,目标输出 x86-64 汇编,逐步加入类型系统、优化与错误恢复。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 手写编译器:从词法到代码生成的技术深度解析
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!