
1. 为什么说“手搓编译器”是程序员的终极修炼如果你在搜索引擎里敲下“编译器”这个词大概率会看到一堆关于如何安装GCC、配置MSVC或者解决某个IDE编译错误的教程。这很正常因为对绝大多数开发者而言编译器是一个“黑盒”——我们输入源代码它吐出可执行文件中间发生了什么似乎并不需要关心。但最近“手搓编译器”这个词在技术社区里热度不低它指向的是一种更底层、更硬核的追求不满足于使用工具而是要亲手揭开这个黑盒理解从文本字符到机器指令的完整魔法。这听起来很吓人对吧汇编、龙书、有限自动机、语法树……这些词足以劝退很多人。但我想告诉你的是构建一个简单的编译器其核心思想远比想象中直观。它不要求你是算法天才更像是在完成一个精密的、多阶段的“翻译”工作。你真正在做的是设计一套规则教会计算机如何阅读你定义的一门新语言哪怕极其简单并把它转换成另一种它能直接执行或进一步处理的形式。这个过程会强迫你以计算机的视角去思考它如何识别一个单词如何理解一句话的结构最终又如何把一句“高级”指令分解成一步步的“低级”动作掌握这个过程的价值远超一个玩具编译器本身。当你调试一段晦涩的语法错误时你会对所用编程语言的设计有恍然大悟的感觉当你处理运算符优先级时你会对表达式求值有全新的认识甚至当你用工具自动生成代码时你也能理解它背后的原理与局限。无论是想深入理解Java的JVM、Python的解释器还是想自己设计一门领域特定语言DSL或是优化前端构建工具编译原理的知识都是那盏关键的灯。本文我将带你抛开恐惧用最直白的方式一步步构建一个能处理简单算术表达式的编译器我们会一起走过词法分析、语法分析直到生成“代码”。你会发现这趟旅程的终点不是恐惧而是通透。2. 编译器的工作流水线一场精密的接力赛在开始写第一行代码之前我们必须像建筑师看蓝图一样看清编译器的整体结构。它不是一个大一统的复杂函数而是一条清晰的分阶段流水线。每个阶段各司其职并将处理结果传递给下一阶段这种“关注点分离”的设计使得每个部分都可以独立理解、实现和优化。一个典型的编译器前端主要包含三个核心阶段我们可以用翻译人类语言来类比词法分析相当于“认单词”。输入是一长串字符比如“price 10 2 * 3”词法分析器Lexer的任务是扫描这些字符根据预定义的规则把它们组合成一个个有意义的“单词”在编译器中称为“词法单元”或“Token”。例如它会识别出price是一个标识符是赋值运算符10、2、3是数字字面量和*是算术运算符。这个阶段会忽略空格、换行、注释这些“空白字符”。语法分析相当于“组句子、看结构”。输入是上一阶段产出的Token流。语法分析器Parser的任务是根据预定义的“语法规则”Grammar检查这些Token的排列顺序是否符合一门有效“句子”的结构并最终构造出一棵“语法树”。这棵树清晰地表达了计算的优先级和结合性。例如对于10 2 * 3语法树会确保乘法节点*是加法节点的子节点直观表示“先乘后加”。中间代码生成相当于“写翻译稿”。输入是语法树。代码生成器Code Generator的任务是遍历这棵树根据每个节点的类型生成等价的“目标代码”。这个目标代码可以是另一种高级语言如C代码、一种抽象的中间表示如三地址码或者我们例子中更简单的、能直接解释执行的指令序列。为什么必须分成这三步想象一下如果你试图直接从字符串“102*3”一口气算出结果并生成代码你的逻辑会很快变得无比混乱。你需要同时操心哪里是数字的结尾、运算符的优先级、括号的匹配。而分层之后Lexer只关心如何切分TokenParser只关心Token之间的结构关系Generator只关心如何映射结构到动作。每一层的复杂度被严格控制这就是软件工程中“分而治之”思想的完美体现。在我们的实战项目中我们将为一种超级简单的语言定义这三个阶段。这门语言只包含整数、四则运算,-,*,/和括号。我们的目标是将诸如“(1 2) * 3 - 4”这样的表达式最终转换成一串可以顺序执行的简单指令。3. 词法分析器教计算机“认字”词法分析是编译器的第一道关卡它的输入是源代码字符串输出是一个个标记好的Token。我们可以把Lexer想象成一个迟钝但严格的书报扫描员它一个字符一个字符地读根据当前读到的字符来决定“当前正在组装的词是什么类型”以及“什么时候一个词组装完了”。3.1 定义我们的Token宇宙首先我们需要为我们的迷你语言定义所有可能的“单词类型”也就是Token类型。这就像为我们的语言编写一本基础词典。# 定义Token类型用枚举类清晰表示 from enum import Enum class TokenType(Enum): # 字面量 NUMBER NUMBER # 整数如 123 # 运算符 PLUS PLUS # MINUS MINUS # - MUL MUL # * DIV DIV # / # 括号 LPAREN LPAREN # ( RPAREN RPAREN # ) # 特殊标记 EOF EOF # 文件结束表示输入已处理完毕每个Token对象不仅需要知道自己的类型还需要记录它对应的原始文本lexeme以及在源代码中的位置便于后续报错。一个简单的Token类可以这样设计class Token: def __init__(self, type_: TokenType, lexeme: str, position: int): self.type type_ self.lexeme lexeme # 原始的字符串如 “123”, “” self.position position # 在输入字符串中的起始索引 def __repr__(self): return fToken({self.type}, “{self.lexeme}”, pos{self.position})3.2 实现扫描器状态驱动的字符处理有了词典接下来就要实现扫描器Scanner的逻辑。它的核心是一个循环遍历输入字符串的每个字符并根据当前字符进入不同的处理逻辑。这里的关键是“状态”思想扫描器始终处于某种状态中例如“正在读取数字”、“正在读取标识符”、“默认状态”并根据新读入的字符决定是留在当前状态、结束当前Token并开始新的还是报错。下面是一个简化但完整的Lexer实现它能够处理数字、运算符和括号class Lexer: def __init__(self, text: str): self.text text # 源代码字符串 self.pos 0 # 当前字符的索引位置 self.current_char self.text[self.pos] if self.text else None # 当前字符 def error(self): raise Exception(fInvalid character at position {self.pos}: “{self.current_char}”) def advance(self): 向前移动一个字符更新current_char。如果到达末尾则设为None。 self.pos 1 if self.pos len(self.text): self.current_char None else: self.current_char self.text[self.pos] def skip_whitespace(self): 跳过所有空白字符空格、制表符、换行。 while self.current_char is not None and self.current_char.isspace(): self.advance() def number(self): 读取一个多位整数直到遇到非数字字符。 start_pos self.pos while self.current_char is not None and self.current_char.isdigit(): self.advance() # 提取数字部分的字符串 num_str self.text[start_pos:self.pos] # 在实际编译器中这里可能需要将字符串转换为整数或浮点数 # 但词法分析阶段通常只负责识别和传递字面量字符串 return Token(TokenType.NUMBER, num_str, start_pos) def get_next_token(self): 主方法获取下一个Token。 while self.current_char is not None: # 跳过空白 if self.current_char.isspace(): self.skip_whitespace() continue # 识别数字 if self.current_char.isdigit(): return self.number() # 识别单字符运算符和括号 if self.current_char : token Token(TokenType.PLUS, , self.pos) self.advance() return token if self.current_char -: token Token(TokenType.MINUS, -, self.pos) self.advance() return token if self.current_char *: token Token(TokenType.MUL, *, self.pos) self.advance() return token if self.current_char /: token Token(TokenType.DIV, /, self.pos) self.advance() return token if self.current_char (: token Token(TokenType.LPAREN, (, self.pos) self.advance() return token if self.current_char ): token Token(TokenType.RPAREN, ), self.pos) self.advance() return token # 如果遇到无法识别的字符抛出错误 self.error() # 所有字符处理完毕返回EOF标记 return Token(TokenType.EOF, , self.pos)实操心得与避坑指南贪心匹配原则在读取数字number方法时我们采用“贪心”策略尽可能多地连续读取数字字符。这是词法分析中的常见模式。对于更复杂的词法单元如浮点数、科学计数法、、等多字符运算符都需要仔细设计这种状态机逻辑。位置信息至关重要Token中保存的position在语法分析报错时极其有用。你可以精确地告诉用户“第X行第Y列附近有语法错误”而不是一个笼统的提示。提前看字符更复杂的Lexer可能需要“向前看”一个或多个字符peek来决定当前Token的边界。例如看到/时需要看下一个字符是否是/单行注释或*多行注释以决定是将其识别为除法运算符还是注释的开始。让我们用这个Lexer测试一下lexer Lexer(“(10 20) * 3”) tokens [] while True: token lexer.get_next_token() tokens.append(token) if token.type TokenType.EOF: break print(tokens)输出会类似于[Token(TokenType.LPAREN, “(”, pos0), Token(TokenType.NUMBER, “10”, pos1), Token(TokenType.PLUS, “”, pos4), Token(TokenType.NUMBER, “20”, pos6), Token(TokenType.RPAREN, “)”, pos8), Token(TokenType.MUL, “*”, pos10), Token(TokenType.NUMBER, “3”, pos12), Token(TokenType.EOF, “”, pos13)]看源代码字符串已经被成功地分解成了一连串有明确类型的Token。词法分析的任务圆满完成为下一阶段的语法分析准备好了结构化的输入。4. 语法分析器构建理解的“树状骨架”拿到Token流之后编译器需要理解它们的结构关系。“10 2 * 3”不是“10”, “”, “2”, “*”, “3”的简单罗列它意味着“10加上2乘以3的结果”。语法分析器Parser的职责就是根据一组形式化的“语法规则”验证Token序列是否构成一个合法的程序并构建出一棵“抽象语法树”Abstract Syntax Tree, AST这棵树以图形化的方式清晰地表达了运算的优先级和嵌套关系。4.1 定义语法规则巴科斯-诺尔范式我们首先需要用一种形式化的方式描述我们语言的语法。最常用的是巴科斯-诺尔范式BNF或其扩展形式EBNF。对于我们简单的算术表达式规则可以这样定义expr : term ( (PLUS | MINUS) term )* term : factor ( (MUL | DIV) factor )* factor : NUMBER | LPAREN expr RPAREN让我来解释一下expr表达式是最高层的规则它由一个term开头后面可以跟零个或多个(加号或减号) term。term项由一个factor开头后面可以跟零个或多个(乘号或除号) factor。factor因子是一个基本单元要么是一个NUMBER数字要么是一个由括号括起来的expr。这里的精妙之处在于优先级和结合性的定义优先级乘法/除法term规则被定义在加法/减法expr规则的“更底层”。在解析时Parser会试图先构造一个term其中可能包含高优先级的*和/然后再处理外层的和-。这自然保证了*和/的优先级高于和-。结合性规则中(PLUS | MINUS) term )*这种“循环”式的定义使得1 2 3会被解析为((1 2) 3)即左结合这是我们期望的算术运算结合性。4.2 递归下降解析用代码模拟语法规则“递归下降”是一种直观的语法分析方法其核心思想是为语法规则中的每个非终结符如expr,term,factor编写一个对应的函数。这个函数的工作就是“吃掉”属于它这个语法单元的Token并返回对应的AST节点。函数内部通过调用其他非终结符的函数即“递归”来匹配更复杂的结构。首先我们需要定义AST的节点类型。一个简单的设计如下class ASTNode: pass class BinOp(ASTNode): 二元操作节点如 左表达式 运算符 右表达式 def __init__(self, left: ASTNode, op: Token, right: ASTNode): self.left left self.op op # 这是一个Token对象如 Token(PLUS, ‘’) self.right right class Num(ASTNode): 数字字面量节点 def __init__(self, token: Token): self.token token self.value int(token.lexeme) # 将字符串转换为整数值接着我们实现递归下降语法分析器。它内部会持有一个Lexer实例并不断从中获取Token。class Parser: def __init__(self, lexer: Lexer): self.lexer lexer self.current_token self.lexer.get_next_token() # 初始化当前Token def error(self): raise Exception(f‘Syntax error at position {self.current_token.position}’) def eat(self, token_type: TokenType): “消耗”当前Token。如果当前Token类型符合预期则获取下一个Token否则报错。 if self.current_token.type token_type: self.current_token self.lexer.get_next_token() else: self.error() def factor(self): 解析因子数字 或 表达式 token self.current_token if token.type TokenType.NUMBER: self.eat(TokenType.NUMBER) return Num(token) # 返回一个数字节点 elif token.type TokenType.LPAREN: self.eat(TokenType.LPAREN) node self.expr() # 递归解析括号内的表达式 self.eat(TokenType.RPAREN) return node else: self.error() def term(self): 解析项因子 ( (* 或 /) 因子 )* node self.factor() # 解析第一个因子 # 循环处理连续的乘除 while self.current_token.type in (TokenType.MUL, TokenType.DIV): op self.current_token if op.type TokenType.MUL: self.eat(TokenType.MUL) elif op.type TokenType.DIV: self.eat(TokenType.DIV) # 解析右边的因子并与之前的节点组成新的二元操作节点 node BinOp(leftnode, opop, rightself.factor()) return node def expr(self): 解析表达式项 ( ( 或 -) 项 )* node self.term() # 解析第一个项 # 循环处理连续的加减 while self.current_token.type in (TokenType.PLUS, TokenType.MINUS): op self.current_token if op.type TokenType.PLUS: self.eat(TokenType.PLUS) elif op.type TokenType.MINUS: self.eat(TokenType.MINUS) node BinOp(leftnode, opop, rightself.term()) return node def parse(self): 解析的入口点返回整个表达式的AST根节点。 return self.expr()核心逻辑拆解factor()函数实现了语法规则中的factor : NUMBER | LPAREN expr RPAREN。它查看当前Token如果是数字就消耗掉并返回一个Num节点如果是左括号就消耗掉左括号递归调用expr()解析括号内的整个表达式然后消耗掉右括号。term()函数实现了term : factor ( (MUL | DIV) factor )*。它先调用factor()得到第一个节点然后进入一个while循环只要当前Token是*或/就消耗掉这个运算符再调用factor()得到右边的节点然后用这两个节点和运算符构造一个新的BinOp节点作为当前节点。这个循环确保了2 * 3 / 4会被正确地左结合解析为((2 * 3) / 4)。expr()函数与term()逻辑完全一致只是它处理的是和-运算符并且其基础单元是term()。这层调用关系expr - term - factor正是优先级实现的本质expr在组合时其操作数已经是处理完所有*和/的term了。让我们解析表达式“(10 20) * 3”lexer Lexer(“(10 20) * 3”) parser Parser(lexer) ast parser.parse() # 我们可以通过一个简单的打印函数来可视化AST def print_ast(node, indent0): prefix ‘ ‘ * indent if isinstance(node, Num): print(f‘{prefix}Num({node.value})’) elif isinstance(node, BinOp): print(f‘{prefix}BinOp({node.op.lexeme})’) print_ast(node.left, indent1) print_ast(node.right, indent1) print_ast(ast)输出将展示一棵树BinOp(*) BinOp() Num(10) Num(20) Num(3)这棵树清晰地告诉我们乘法*是根节点它的左子树是一个加法节点右子树是数字3。加法节点的左右子树分别是数字10和20。这完美地反映了(10 20) * 3的运算结构。语法分析成功地将线性的Token序列转换为了层次化的树形结构为最终的代码生成铺平了道路。5. 解释执行与代码生成让树“跑”起来现在我们拥有了一棵结构清晰的AST最后一步就是让它产生“效果”。对于编译器来说这一步通常是生成另一种形式的代码如汇编、字节码。但为了最直观地理解这个过程我们先实现一个“解释器”它直接遍历AST并计算表达式的值。理解了解释器再过渡到代码生成器就会非常自然。5.1 解释器深度优先遍历求值解释器的原理是“后序遍历”AST对于一个BinOp节点必须先计算出其左子树的值和右子树的值然后根据运算符op执行相应的计算。对于一个Num节点直接返回其存储的数值即可。这是一个典型的递归过程。class Interpreter: def __init__(self, parser: Parser): self.parser parser def visit(self, node): 访问AST节点的分发方法。根据节点类型调用不同的处理方法。 method_name ‘visit_‘ type(node).__name__ visitor getattr(self, method_name, self.generic_visit) return visitor(node) def generic_visit(self, node): raise Exception(f‘No visit_{type(node).__name__} method’) def visit_Num(self, node): 访问数字节点直接返回值。 return node.value def visit_BinOp(self, node): 访问二元操作节点先递归计算左右子树再执行运算。 left_val self.visit(node.left) right_val self.visit(node.right) if node.op.type TokenType.PLUS: return left_val right_val elif node.op.type TokenType.MINUS: return left_val - right_val elif node.op.type TokenType.MUL: return left_val * right_val elif node.op.type TokenType.DIV: return left_val // right_val # 使用整数除法 else: raise Exception(‘Invalid operator’) def interpret(self): 解释执行的入口点。 tree self.parser.parse() # 获取AST return self.visit(tree) # 从根节点开始访问并计算使用方式非常简单lexer Lexer(“(10 20) * 3”) parser Parser(lexer) interpreter Interpreter(parser) result interpreter.interpret() print(f‘Result: {result}’) # 输出: Result: 90这个解释器完美地实现了我们的计算器。但它的局限性也很明显它只是“计算”没有“生成”任何可以留存或再次执行的代码。它混合了“遍历”和“执行”的逻辑。5.2 代码生成器生成三地址码一个更接近传统编译器的做法是“代码生成”。我们不直接计算而是遍历AST生成一系列简单的、接近机器指令的“中间代码”。一种非常经典且简单的中间表示是“三地址码”Three-Address Code, TAC。每条三地址码指令的形式通常是x y op z其中op是一个二元运算符y和z是操作数可以是变量名或临时变量x是存放结果的临时变量。例如对于表达式(10 20) * 3生成的三地址码序列可能是t1 10 20 t2 t1 * 3最终整个表达式的结果存放在最后一个临时变量t2中。我们需要修改我们的访问器让它不再返回值而是生成指令并返回存放结果的变量名临时变量。class CodeGenerator: def __init__(self, parser: Parser): self.parser parser self.temp_count 0 # 用于生成唯一的临时变量名如 t0, t1, t2... self.code [] # 存储生成的三地址码指令列表 def new_temp(self): 生成一个新的临时变量名。 temp_name f‘t{self.temp_count}’ self.temp_count 1 return temp_name def visit(self, node): method_name ‘visit_‘ type(node).__name__ visitor getattr(self, method_name, self.generic_visit) return visitor(node) def generic_visit(self, node): raise Exception(f‘No visit_{type(node).__name__} method’) def visit_Num(self, node): 访问数字节点数字本身就是一个操作数直接返回它的字符串形式。 return str(node.value) # 返回 “10” def visit_BinOp(self, node): 访问二元操作节点递归生成左右操作数的代码并生成一条新指令。 # 生成左操作数的代码并得到左操作数的值变量名或数字 left_operand self.visit(node.left) # 生成右操作数的代码并得到右操作数的值 right_operand self.visit(node.right) # 为本次运算的结果创建一个新的临时变量 result_temp self.new_temp() # 根据运算符生成对应的三地址码指令 if node.op.type TokenType.PLUS: op ‘’ elif node.op.type TokenType.MINUS: op ‘-’ elif node.op.type TokenType.MUL: op ‘*’ elif node.op.type TokenType.DIV: op ‘/’ else: raise Exception(‘Invalid operator’) # 将生成的指令添加到代码列表中 instruction f‘{result_temp} {left_operand} {op} {right_operand}’ self.code.append(instruction) # 返回这个临时变量的名字作为这个子树表达式的结果 return result_temp def generate(self): 代码生成的入口点。返回生成的三地址码列表。 tree self.parser.parse() final_temp self.visit(tree) # 从根节点开始访问最终返回存放总结果的临时变量名 # 可以选择性地添加一条返回最终结果的指令例如 “return t2” # self.code.append(f‘return {final_temp}’) return self.code让我们生成“(10 20) * 3”的代码lexer Lexer(“(10 20) * 3”) parser Parser(lexer) generator CodeGenerator(parser) tac generator.generate() for instr in tac: print(instr)输出t0 10 20 t1 t0 * 3从解释到生成的跨越意义分离关注点代码生成器只负责“生成”指令不负责“执行”。生成的指令序列三地址码是一个独立的数据结构可以被存储、优化、翻译成不同的目标代码如x86汇编、LLVM IR、Java字节码。优化空间一旦有了中间表示我们就可以在生成最终代码前对其进行优化。例如常量折叠t0 10 20可以直接优化为t0 30、公共子表达式消除等优化技术都是在类似的三地址码或更高级的中间表示上进行的。目标无关同一个AST我们可以写不同的代码生成器后端分别生成x86汇编、ARM汇编或者Python代码。中间表示是连接前端语法分析和后端目标代码生成的桥梁。这个简单的代码生成器已经具备了真实编译器的核心雏形。你可以扩展它支持变量赋值将结果存入符号表、控制流语句生成跳转指令、函数调用等逐步构建出一门真正可用的玩具语言编译器。6. 从玩具到实用扩展思路与避坑实践我们构建了一个能处理整数四则运算的微型编译器前端。虽然简单但它完整地演示了编译器核心三阶段的协作流程。如果你想继续深入把它变成一个更有趣、更实用的项目以下是一些扩展方向和实践中必然会遇到的“坑”及其应对策略。6.1 功能扩展路线图支持变量与赋值词法分析增加标识符IDENTIFIERToken类型和赋值运算符。语法分析增加新的语法规则如statement : IDENTIFIER ‘’ expr和factor : NUMBER | IDENTIFIER | LPAREN expr RPAREN。语义分析新阶段需要维护一个“符号表”来记录变量的定义和使用。在解析到标识符时检查它是否已定义对于读取或是否重复定义对于赋值根据语言规则决定。代码生成生成从内存/寄存器加载变量值以及将计算结果存储到变量对应的内存位置的指令。支持控制流if/while词法分析增加关键字Token如IF,ELSE,WHILE,TRUE,FALSE和比较运算符,!,,等。语法分析增加条件表达式、语句块、if语句、while语句的规则。AST需要新增IfNode,WhileNode,CompareNode等类型。代码生成这是关键一步。需要生成条件跳转和无条件跳转指令。三地址码需要支持带标签的指令如if t0 0 goto L1和goto L2以及标签定义L1:。这要求代码生成器能够管理标签的生成和引用。支持函数定义与调用词法分析/语法分析增加DEF,RETURN,,等Token和相应的语法规则。语义分析符号表需要区分不同作用域全局、函数局部。需要处理参数传递、返回值。代码生成涉及调用约定参数放在哪里返回值放在哪里、栈帧管理局部变量存储、跳转到函数入口和返回地址保存。这是编译器设计中一个相对复杂的部分。6.2 实战中常见的“坑”与解决之道错误恢复与友好报错问题我们目前的Parser在遇到第一个语法错误时就直接抛出异常崩溃这对用户很不友好。真正的编译器会尝试从错误中恢复继续解析后续内容以报告尽可能多的错误。策略实现简单的错误恢复机制。例如在expr、term等函数中当遇到意外Token时可以记录错误然后尝试“同步”到下一个安全点如分号、右括号或语句开始的关键字然后继续解析。同时结合Lexer记录的位置信息给出精确到行列的错误提示。左递归与运算符优先级问题我们使用了expr: term ((PLUS|MINUS) term)*这种“右递归”形式来定义表达式它天然是左结合的。但如果你尝试用直接的左递归如expr: expr PLUS term来写递归下降Parser会导致无限递归。策略递归下降文法通常需要消除左递归。我们的写法称为“扩展巴科斯范式”就是一种标准解决方案。对于更复杂的优先级比如新增一个指数运算符**优先级高于乘除只需在优先级层次中插入新的一层即可例如power : factor ( ‘**’ factor )*然后让term基于power构建。词法分析中的歧义问题如何区分等于和赋值如何区分关键字如if和标识符如iff策略采用“最长匹配”原则和“关键字表”。对于Lexer在看到第一个后应该向前看一个字符如果是就组合成Token否则就是单独的Token。对于关键字可以在识别出一个完整的标识符字符串后去一个预定义的关键字表中查找如果找到就返回对应的关键字Token类型否则返回标识符Token类型。AST设计的扩展性问题随着语言特性增加不断修改visit方法会变得混乱。策略使用“访问者模式”。为AST定义一个通用的accept(visitor)方法每个节点类实现这个方法去调用visitor的对应visit_Xxx方法。这样当你需要新增一种处理AST的方式如另一种代码生成器、类型检查器、代码格式化器时只需实现一个新的visitor类而无需修改AST节点类本身。这极大地提高了代码的模块化和可维护性。构建编译器是一个系统工程每一步的决策都会影响后续的复杂度。我的建议是从一个像本文这样的极小核心开始每次只添加一个特性并确保每个特性在词法、语法、语义、代码生成各阶段都完整通过。在这个过程中你会对编程语言的设计、工具的构建有极其深刻和具体的理解这种理解是阅读任何教科书都无法替代的。当你亲手让一门自己定义的语言“活”起来时那种成就感正是“手搓编译器”这项终极修炼的魅力所在。