1. 为什么我们要“手搓”一个编译器?
如果你是一名程序员,每天打交道最多的可能就是各种编程语言和它们的编译器。无论是写C++时用的gcc/clang,还是写Java时用的javac,甚至是写Python时用的解释器(它内部也包含编译步骤),编译器都是将我们人类可读的代码翻译成机器可执行指令的核心工具。但很多时候,它就像一个黑盒——我们输入源代码,它输出可执行文件或报错信息,中间发生了什么,对很多人来说是个谜。
“从零开始构建一个简单的编译器”这个想法,听起来像是计算机科学专业学生的“毕业设计”级挑战,充满了神秘感和高门槛。但实际上,拆解开来,它的核心流程非常清晰,并且每一步都有成熟的、可理解的理论和工具支撑。自己动手实现一遍,是理解编程语言本质、提升对代码底层认知最有效的方式。这不仅仅是理论学习,更是一次深刻的工程实践。你会真正理解,为什么代码里多一个分号编译器会报错,一个简单的 a = b + c 在机器眼里到底经历了什么,以及那些高级语言特性(如闭包、泛型)背后到底付出了怎样的编译代价。
最近,“手搓编译器”也成了技术圈里的一个热词,不少开发者开始尝试这个挑战,并将其视为深入理解计算机系统的“成人礼”。这个过程能带给你的,远不止于一个能处理 return 123; 的小玩具,而是一整套分析复杂问题、设计数据结构、处理边界情况的系统工程思维。接下来,我们就抛开复杂的理论公式,用最直白的思路和Python代码,一步步搭建起一个能处理简单语句的微型编译器。我们的目标语言极其简单,只包含一个返回整数的main函数,但麻雀虽小,五脏俱全,词法分析、语法分析、目标代码生成这三个核心阶段一个不少。
2. 编译器的核心流水线:三阶段模型
在开始写代码之前,我们必须先建立起对编译器工作流程的宏观认识。一个典型的编译器,就像一条生产流水线,源代码是原材料,经过多个车间的加工,最终变成目标产品(汇编或机器码)。我们即将构建的微型编译器,采用最经典的三阶段模型:
源代码(字节流) -> 词法分析器 -> Token流 -> 语法分析器 -> 抽象语法树(AST) -> 代码生成器 -> 目标代码(汇编)
这个模型清晰地将编译过程解耦成了三个相对独立的模块,每个模块只关心自己的输入和输出格式,这使得开发、测试和维护都变得更容易。我们来详细看看每个车间的职责。
2.1 第一阶段:词法分析器——从字符到单词
想象一下你在阅读一段英文句子。你不会一个字母一个字母地去理解,而是本能地将连续的字母组合成有意义的单词,比如“compiler”。词法分析器(Lexer)干的就是这个活儿。它的输入是源代码文件的一串原始字节(比如 "int main(){return 123;}" 对应的ASCII或UTF-8字节),输出则是一个个有分类、有内容的“单词”,在编译原理中称为 词法单元 或 Token 。
为什么需要这一步?因为对于语法分析器来说,直接处理 'i' , 'n' , 't' 这三个连续的字符是低效且困难的。词法分析器会识别出它们组成了关键字 int ,并打上“关键字”的标签。同样,它会识别出 123 是一个整数常量, main 是一个标识符, { 、 } 、 ; 是特定的符号。
每个Token通常包含两部分信息:
- 类型(Token Type) :表明这个Token属于哪一类,如
关键字(KEYWORD)、标识符(IDENTIFIER)、整数常量(INTEGER)、左大括号(LBRACE)等。 - 值(Lexeme) :Token对应的原始字符串,如对于整数常量
123,其值就是字符串"123"。
词法分析的实现,核心是定义一系列 正则表达式 规则,来描述每一类Token长什么样。例如,整数常量可以定义为 [0-9]+ (一个或多个数字),标识符可以定义为 [a-zA-Z_][a-zA-Z0-9_]* (以字母或下划线开头,后接字母、数字或下划线)。Lexer的工作就是从左到右扫描字节流,尽可能匹配最长的规则,然后切分出一个Token。
2.2 第二阶段:语法分析器——从单词到句子结构
有了单词(Token)流,接下来就要理解句子的结构。这就是语法分析器(Parser)的任务。它的输入是Token流,输出是一棵 抽象语法树 。
为什么是树形结构?因为程序本身具有嵌套的层次结构。例如, return 123; 这条语句,它位于 main 函数体内,而 main 函数又是整个程序的一部分。树形结构能完美地表达这种“包含”与“从属”关系。
语法分析器依据的是语言的 语法规则 ,通常用 上下文无关文法 来描述。对于我们这个超简化的语言,规则可以这样写:
program : function
function : type IDENTIFIER '(' ')' '{' statement '}'
type : 'int'
statement : 'return' expression ';'
expression : INTEGER
这些规则定义了什么是合法的程序。Parser会检查输入的Token流是否符合这些规则。如果符合,它就根据规则构建出AST;如果不符合(比如缺少分号或括号不匹配),它就会报语法错误。
AST是源代码的抽象表示,它省略了一些对后续阶段无关紧要的细节(比如具体的括号、分号Token),只保留程序逻辑结构的骨架。例如,对于 int main(){return 123;} ,其AST可能类似于:
Program
└── Function(name="main", return_type="int")
└── Body
└── ReturnStmt
└── IntegerLiteral(value=123)
2.3 第三阶段:代码生成器——从结构到指令
AST清晰地表达了程序要“做什么”。代码生成器(Code Generator)的任务,就是把“做什么”翻译成目标平台(我们选择RISC-V)的汇编指令,告诉机器“怎么做”。
这是从高级抽象到低级具体的一步。我们需要遍历AST,针对每种类型的节点(如函数节点、返回语句节点、整数常量节点)生成对应的汇编代码片段。例如:
- 遇到整数常量节点(IntegerLiteral) :生

9980

被折叠的 条评论
为什么被折叠?



