从零手搓编译器:Python实现词法分析、语法分析与代码生成
1. 项目概述为什么我们要“手搓”一个编译器“编译器”这个词听起来总是带着一层神秘的面纱仿佛是高阶程序员的专属领域。每当看到GCC、Clang这些庞然大物我们很容易产生一种错觉构建一个编译器需要极其深厚的计算机科学功底和庞大的团队。但事实并非如此。今天我想和你分享的就是如何从零开始用相对简单的思路和代码构建一个能真正工作的“玩具级”编译器。这个过程与其说是在造一个工业级工具不如说是一场深入计算机语言核心的“解剖实验”。这个项目的核心目标不是与GCC竞争而是彻底理解“从文本到机器可执行指令”这条流水线究竟是如何运转的。我们将亲手实现经典的编译器三阶段词法分析、语法分析和代码生成。你会看到一段诸如a 3 5的简单文本是如何被拆解成单词词法分析如何被组织成一棵体现运算优先级的结构树语法分析最后又是如何被翻译成类似汇编的底层指令或另一种高级语言代码代码生成的。这个过程能极大地加深你对编程语言本身、对代码执行过程的理解这种理解是单纯使用编译器无法获得的。无论你是对编译原理感到好奇的学生还是希望夯实底层知识的中级开发者甚至是想要自己设计一门领域特定语言DSL的探索者这个“手搓编译器”的旅程都将是一次宝贵的实践。它剥离了复杂优化和庞大生态直击核心概念让你获得“造物主”般的视角。接下来我们就从最基础的思路设计开始一步步把它搭建起来。2. 整体设计与核心思路拆解在动手写第一行代码之前我们必须把整个编译器的蓝图设计清楚。一个典型的编译器前端流程可以看作一个精密的翻译管道源代码文本从一端流入经过层层处理最终从另一端流出目标代码。我们的“简单编译器”将严格遵循这个管道模型但每个环节都做最大程度的简化。2.1 核心架构三段式流水线我们的编译器架构采用最经典的三段式设计这也是绝大多数教学编译器和早期工业编译器的基础模型。词法分析器这是管道的第一个环节。它的任务像是一个敏锐的“单词扫描仪”。我们写的源代码本质上是一个长长的字符串里面混杂着关键字、标识符、数字、运算符等各种元素。词法分析器的工作就是从左到右扫描这个字符串根据预定义的规则比如数字由连续的数字字符组成标识符以字母开头等将它们切割成一个个有意义的独立单元这些单元被称为“词法单元”或“Token”。例如对于sum 10 count词法分析器会产出五个Token[标识符“sum”], [赋值运算符“”], [数字“10”], [加法运算符“”], [标识符“count”]。这个阶段会忽略空格、换行和注释这些无关紧要的字符。语法分析器拿到Token序列后语法分析器扮演了“结构工程师”的角色。它根据预先定义好的“语法规则”通常用上下文无关文法描述检查Token序列是否符合语言的结构规范并构建出一棵“抽象语法树”。这棵树清晰地表达了代码的嵌套层次和运算优先级。例如对于10 2 * 3语法分析器能识别出乘法*的优先级高于加法从而生成一棵树其根节点是左孩子是10右孩子是一棵以*为根、2和3为孩子的子树。这棵树是后续所有处理的基础数据结构。代码生成器这是最后的“翻译官”。它遍历抽象语法树根据每个节点的类型生成等价的目标代码。目标代码的选择很灵活为了简单起见我们可以选择生成一种非常简单的“栈式虚拟机指令”或者生成另一种高级语言如Python的代码。例如对于上面的AST代码生成器可能会生成这样的指令序列“将10压入栈将2压入栈将3压入栈执行乘法弹出栈顶两个元素相乘结果压回栈执行加法”。如果生成Python代码那就是简单的10 2 * 3。注意我们这里刻意省略了“语义分析”如类型检查和“优化”这两个在工业级编译器中至关重要的阶段。这是为了保持项目的核心聚焦和可控性。我们的目标是理解主干流程枝繁叶茂的优化可以留待日后探索。2.2 语言定义我们编译什么在构建编译器之前必须先定义它要编译的语言。我们设计一个超级简化的算术表达式语言它只包含以下元素整数如0,42,100。变量由字母开头的字母数字串组成如x,total,count1。运算符四则运算,-,*,/以及赋值。括号(和)用于改变运算优先级。语句一个程序由多个“赋值语句”组成每个语句形式为变量 表达式;。例如下面就是一个合法的“程序”a 10; b 20; result a b * 2;这个语言虽然简单但已经具备了构成一门编程语言最核心的要素数据整数、变量、运算和顺序执行。以此为基础我们足以演示完整的编译流程。2.3 工具选型为什么用Python实现语言的选择很多C、Java、Go都可以。但我强烈推荐使用Python作为第一次“手搓编译器”的实现语言原因如下快速原型Python语法简洁能让我们将精力集中在算法和逻辑本身而非内存管理、复杂类型声明等细节上。强大的内置数据结构列表、字典、字符串处理等功能非常适合实现Token流、语法树等结构。易于表达和调试我们可以轻松地打印出Token列表、以缩进形式可视化AST每一步的中间结果都清晰可见极大降低了调试难度。目的契合我们的目标是教学和理解而非追求极致性能。Python的慢速在这个场景下完全不是问题。当然如果你对性能有执念或者想更贴近底层用C语言重写一遍将是绝佳的进阶练习。但第一步请先用Python把路走通。3. 第一阶段实战构建词法分析器词法分析器也叫扫描器是整个编译过程的“开路先锋”。它的实现思路主要有两种手动编写状态机或者使用生成工具如Lex。为了彻底理解原理我们选择手动实现一个基于有限状态自动机思想的分析器。3.1 Token的设计与定义首先我们需要定义我们的语言中有哪些类型的Token。我们可以用一个Python类或命名元组来代表一个Token它至少包含两个信息类型Type和值Value。class Token: def __init__(self, type_, valueNone): self.type type_ # Token类型如 ‘INTEGER‘, ’PLUS‘ self.value value # Token的原始字符串值如 ‘123‘, ’‘ def __repr__(self): return f‘Token({self.type}, {repr(self.value)})‘接下来我们为迷你语言定义Token类型。我们可以用字符串常量来表示# Token 类型 INTEGER ‘INTEGER‘ PLUS ‘PLUS‘ MINUS ‘MINUS‘ MUL ‘MUL‘ DIV ‘DIV‘ LPAREN ‘LPAREN‘ # 左括号 ( RPAREN ‘RPAREN‘ # 右括号 ) ASSIGN ‘ASSIGN‘ # 赋值 ID ‘ID‘ # 标识符如变量名 SEMI ‘SEMI‘ # 分号 ; EOF ‘EOF‘ # 文件结束标志3.2 手动实现扫描器我们将编写一个Lexer类它接收源代码字符串并提供一个get_next_token()方法每次调用都返回下一个Token。class Lexer: def __init__(self, text): self.text text # 源代码字符串 self.pos 0 # 当前字符索引 self.current_char self.text[self.pos] if self.text else None def error(self): raise Exception(‘Invalid character‘) def advance(self): 移动‘pos‘指针获取下一个字符。 self.pos 1 if self.pos len(self.text) - 1: 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 integer(self): 读取一个多位整数。 result ‘‘ while self.current_char is not None and self.current_char.isdigit(): result self.current_char self.advance() return int(result) def identifier(self): 读取一个标识符变量名。 result ‘‘ # 标识符以字母或下划线开头 while self.current_char is not None and (self.current_char.isalnum() or self.current_char ‘_‘): result self.current_char self.advance() return result 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 Token(INTEGER, self.integer()) if self.current_char.isalpha() or self.current_char ‘_‘: id_name self.identifier() # 这里可以检查是否是关键字我们语言简单暂无关键字 return Token(ID, id_name) # 处理单字符操作符 if self.current_char ‘‘: self.advance() return Token(PLUS, ‘‘) if self.current_char ‘-‘: self.advance() return Token(MINUS, ‘-‘) if self.current_char ‘*‘: self.advance() return Token(MUL, ‘*‘) if self.current_char ‘/‘: self.advance() return Token(DIV, ‘/‘) if self.current_char ‘(‘: self.advance() return Token(LPAREN, ‘(‘) if self.current_char ‘)‘: self.advance() return Token(RPAREN, ‘)‘) if self.current_char ‘‘: self.advance() return Token(ASSIGN, ‘‘) if self.current_char ‘;‘: self.advance() return Token(SEMI, ‘;‘) self.error() # 遇到无法识别的字符 # 源代码已全部处理完毕 return Token(EOF)实操心得在手动编写词法分析器时最容易出错的地方就是状态切换。比如在读取完一个整数123后current_char已经指向了数字后面的字符此时一定要确保get_next_token方法的下一次调用是从这个新位置开始而不是重复读取。我们的advance()方法很好地管理了这个指针。另外skip_whitespace()中的continue语句至关重要它确保在跳过空白后立即重新开始判断字符类型而不是直接返回。3.3 测试词法分析器让我们用一段简单的代码测试一下我们的词法分析器是否工作正常。def test_lexer(): code ‘‘‘ x 42; y (10 2) * 3; ‘‘‘ lexer Lexer(code) tokens [] while True: token lexer.get_next_token() tokens.append(token) if token.type EOF: break print(tokens) # 期望输出类似 # [Token(ID, ‘x‘), Token(ASSIGN, ‘‘), Token(INTEGER, 42), Token(SEMI, ‘;‘), # Token(ID, ‘y‘), Token(ASSIGN, ‘‘), Token(LPAREN, ‘(‘), Token(INTEGER, 10), ... , Token(EOF)]如果测试通过恭喜你你已经成功地将源代码字符串转换成了结构化的Token流。这是理解编译器工作的第一步也是最直观的一步。接下来我们要让这些零散的Token变得有层次、有结构。4. 第二阶段实战构建语法分析器语法分析是编译器的“大脑”它负责理解Token序列的结构性含义。我们采用“递归下降”分析法来实现这是最直观、最适合手工编写语法分析器的方法。它的核心思想是为语法规则中的每个非终结符如“表达式”、“项”编写一个对应的解析函数。4.1 定义语法规则首先我们需要用形式化的方式描述我们迷你语言的语法。我们使用巴科斯范式BNF或其扩展形式EBNF来定义。对于我们包含赋值语句的语言规则可以这样写program : statement statement : ID ‘‘ expr ‘;‘ expr : term ((PLUS | MINUS) term)* term : factor ((MUL | DIV) factor)* factor : INTEGER | ID | LPAREN expr RPAREN解释一下program程序由至少一条statement语句组成。statement语句是一个标识符变量名后跟赋值号再跟一个表达式expr最后以分号;结束。expr表达式由一个term项开头后面可以跟零个或多个PLUS|MINUS term即加减运算。term项由一个factor因子开头后面可以跟零个或多个MUL|DIV factor即乘除运算。factor因子可以是一个整数、一个标识符变量或者一个用括号括起来的表达式。这种分层定义巧妙地隐含了运算符的优先级括号factor最高其次是乘除term最后是加减expr。赋值的优先级最低并且在语句级处理。4.2 实现递归下降语法分析器我们将创建一个Parser类它内部封装一个词法分析器实例并提供一个current_token来跟踪当前正在查看的Token。class Parser: def __init__(self, lexer): self.lexer lexer self.current_token self.lexer.get_next_token() # 初始化获取第一个Token def error(self): raise Exception(‘Invalid syntax‘) def eat(self, token_type): “消耗”当前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 INTEGER: self.eat(INTEGER) return NumNode(token.value) # 返回一个数字节点 elif token.type ID: self.eat(ID) return VarNode(token.value) # 返回一个变量节点 elif token.type LPAREN: self.eat(LPAREN) node self.expr() # 递归解析括号内的表达式 self.eat(RPAREN) return node else: self.error() def term(self): 解析项因子 ((乘|除) 因子)* node self.factor() # 解析第一个因子 # 循环处理连续的乘除运算 while self.current_token.type in (MUL, DIV): token self.current_token if token.type MUL: self.eat(MUL) elif token.type DIV: self.eat(DIV) # 解析右边的因子并与之前的节点构成一个新的二元运算节点 node BinOpNode(leftnode, optoken.value, rightself.factor()) return node def expr(self): 解析表达式项 ((加|减) 项)* node self.term() # 解析第一个项 # 循环处理连续的加减运算 while self.current_token.type in (PLUS, MINUS): token self.current_token if token.type PLUS: self.eat(PLUS) elif token.type MINUS: self.eat(MINUS) node BinOpNode(leftnode, optoken.value, rightself.term()) return node def statement(self): 解析语句ID 表达式 ; # 当前Token应该是一个标识符变量名 var_name self.current_token.value self.eat(ID) self.eat(ASSIGN) # 消耗掉 ‘‘ expr_node self.expr() # 解析等号右边的表达式 self.eat(SEMI) # 消耗掉 ‘;‘ return AssignNode(var_name, expr_node) # 返回一个赋值语句节点 def program(self): 解析程序语句 statements [] # 持续解析语句直到遇到文件结束符 while self.current_token.type ! EOF: statements.append(self.statement()) return ProgramNode(statements) # 返回程序根节点上面的代码中我们引入了几个AST节点类用于在内存中构建树形结构class ASTNode: 所有AST节点的基类。 pass class NumNode(ASTNode): def __init__(self, value): self.value value class VarNode(ASTNode): def __init__(self, name): self.name name class BinOpNode(ASTNode): def __init__(self, left, op, right): self.left left self.op op self.right right class AssignNode(ASTNode): def __init__(self, var_name, expr_node): self.var_name var_name self.expr_node expr_node class ProgramNode(ASTNode): def __init__(self, statements): self.statements statements注意事项递归下降分析器的函数调用栈直接对应了语法规则的嵌套层次。expr调用termterm调用factorfactor可能递归调用expr当遇到括号时。这种清晰的对应关系使得代码非常易于理解和调试。关键在于每个函数都“承诺”解析完它负责的语法单元并将当前Token推进到该单元之后的位置。4.3 可视化抽象语法树为了直观地看到解析结果我们可以为AST实现一个简单的可视化方法。def print_ast(node, indent0): 递归打印AST用缩进表示层级。 prefix ‘ ‘ * indent if isinstance(node, NumNode): print(f‘{prefix}Num({node.value})‘) elif isinstance(node, VarNode): print(f‘{prefix}Var({node.name})‘) elif isinstance(node, BinOpNode): print(f‘{prefix}BinOp({node.op})‘) print_ast(node.left, indent 1) print_ast(node.right, indent 1) elif isinstance(node, AssignNode): print(f‘{prefix}Assign(to: {node.var_name})‘) print_ast(node.expr_node, indent 1) elif isinstance(node, ProgramNode): print(f‘{prefix}Program‘) for stmt in node.statements: print_ast(stmt, indent 1) # 测试语法分析 code ‘‘‘ a 3 5 * 2; ‘‘‘ lexer Lexer(code) parser Parser(lexer) ast parser.program() print_ast(ast)运行上述代码你将会看到类似下面的输出它清晰地展示了5 * 2先结合然后再与3相加的树形结构Program Assign(to: a) BinOp() Num(3) BinOp(*) Num(5) Num(2)至此我们已经成功地将线性的Token序列转换成了层次化的树形结构。这棵AST完整地保留了源代码的语义并且消除了括号、运算符优先级等语法细节是进行代码生成的完美起点。5. 第三阶段实战实现代码生成器代码生成器是编译器的“产出部门”它遍历AST为每个节点生成对应的目标代码。目标代码的形式多种多样为了最大化教学意义并保持简单我们这里实现两种一种是生成“三地址码”这种中间表示另一种是生成可执行的Python代码。5.1 生成三地址码三地址码是一种非常常见的中间表示形式每条指令最多涉及三个地址操作数。它非常接近底层汇编但又保持了硬件无关性。通常形式如result arg1 op arg2。我们为AST节点类添加一个generate_tac方法该方法返回一个指令列表。class NumNode(ASTNode): # ... 其他代码同上 ... def generate_tac(self, code_gen): # 为数字生成一个临时变量名来保存它的值 temp_var code_gen.new_temp() code_gen.emit(f‘{temp_var} {self.value}‘) return temp_var # 返回存放该值的变量名 class VarNode(ASTNode): # ... 其他代码同上 ... def generate_tac(self, code_gen): # 变量节点直接返回变量名本身 return self.name class BinOpNode(ASTNode): # ... 其他代码同上 ... def generate_tac(self, code_gen): # 递归生成左右操作数的代码并获取它们的结果变量 left_var self.left.generate_tac(code_gen) right_var self.right.generate_tac(code_gen) # 为本次运算结果创建一个新的临时变量 result_var code_gen.new_temp() # 生成三地址码指令 code_gen.emit(f‘{result_var} {left_var} {self.op} {right_var}‘) return result_var # 返回本次运算结果的变量名 class AssignNode(ASTNode): # ... 其他代码同上 ... def generate_tac(self, code_gen): # 生成右侧表达式的代码并获取结果变量 expr_var self.expr_node.generate_tac(code_gen) # 生成赋值指令 code_gen.emit(f‘{self.var_name} {expr_var}‘) return self.var_name class ProgramNode(ASTNode): # ... 其他代码同上 ... def generate_tac(self): code_gen CodeGenerator() for stmt in self.statements: stmt.generate_tac(code_gen) return code_gen.instructions class CodeGenerator: 辅助类用于管理临时变量和指令序列。 def __init__(self): self.instructions [] self.temp_counter 0 def new_temp(self): 生成一个新的临时变量名如 t1, t2... self.temp_counter 1 return f‘t{self.temp_counter}‘ def emit(self, instruction): 添加一条指令。 self.instructions.append(instruction)现在我们可以用之前的AST来生成三地址码了code ‘‘‘ a 3 5 * 2; ‘‘‘ lexer Lexer(code) parser Parser(lexer) ast parser.program() tac_instructions ast.generate_tac() for instr in tac_instructions: print(instr)输出将会是t1 5 t2 2 t3 t1 * t2 t4 3 t5 t4 t3 a t5这些指令顺序执行就能计算出a的最终值。三地址码非常利于后续的优化比如删除t4 3这样冗余的赋值和翻译成真正的机器码。5.2 生成Python代码生成另一种高级语言如Python的代码则简单直接得多本质上就是对AST进行递归拼接字符串。class NumNode(ASTNode): # ... 其他代码同上 ... def to_python(self): return str(self.value) class VarNode(ASTNode): # ... 其他代码同上 ... def to_python(self): return self.name class BinOpNode(ASTNode): # ... 其他代码同上 ... def to_python(self): # 注意需要给子表达式加括号以保证优先级但我们的AST结构已经保证了优先级所以可以不加。 # 但为了安全可以给非叶子节点都加上括号。 left_str self.left.to_python() right_str self.right.to_python() return f‘({left_str} {self.op} {right_str})‘ class AssignNode(ASTNode): # ... 其他代码同上 ... def to_python(self): return f‘{self.var_name} {self.expr_node.to_python()}‘ class ProgramNode(ASTNode): # ... 其他代码同上 ... def to_python(self): lines [stmt.to_python() for stmt in self.statements] return ‘\n‘.join(lines) # 测试生成Python代码 code ‘‘‘ a 3 5 * 2; b (10 - 2) / 4; ‘‘‘ lexer Lexer(code) parser Parser(lexer) ast parser.program() python_code ast.to_python() print(“生成的Python代码“) print(python_code) print(“\n执行结果“) # 动态执行生成的代码注意在生产环境中需谨慎使用eval/exec exec_globals {} exec(python_code, exec_globals) print(f“a {exec_globals.get(‘a‘)}“) print(f“b {exec_globals.get(‘b‘)}“)输出生成的Python代码 a (3 (5 * 2)) b ((10 - 2) / 4) 执行结果 a 13 b 2.0实操心得代码生成阶段最需要小心的是“上下文”问题。在生成三地址码时我们通过CodeGenerator类来管理全局的指令列表和临时变量计数器。在生成Python这类语言时则要特别注意运算符优先级和括号的添加。虽然我们的AST结构保证了优先级但在转换为字符串时为每个二元运算节点都加上括号是一种简单且安全的策略虽然可能产生多余的括号但保证了语义绝对正确。6. 集成测试与问题排查实录将词法分析、语法分析和代码生成三个阶段串联起来我们就得到了一个完整的编译器流水线。让我们构建一个简单的Compiler类来封装整个过程并进行端到端的测试。6.1 构建完整的编译器类class SimpleCompiler: def __init__(self): pass def compile(self, source_code, target‘python‘): 编译源代码。 Args: source_code: 源代码字符串。 target: 目标代码类型‘python‘ 或 ‘tac‘三地址码。 Returns: 生成的目标代码字符串。 # 1. 词法分析 lexer Lexer(source_code) # 2. 语法分析 parser Parser(lexer) ast parser.program() # 3. 代码生成 if target ‘python‘: return ast.to_python() elif target ‘tac‘: instructions ast.generate_tac() return ‘\n‘.join(instructions) else: raise ValueError(f“Unsupported target: {target}“) # 使用编译器 compiler SimpleCompiler() source ‘‘‘ x 1; y 2; sum x y * 10; ‘‘‘ print(“ 生成Python代码 ) py_output compiler.compile(source, ‘python‘) print(py_output) print(“\n 生成三地址码 ) tac_output compiler.compile(source, ‘tac‘) print(tac_output)6.2 常见问题与调试技巧在“手搓编译器”的过程中你几乎一定会遇到下面这些问题。这里我把自己踩过的坑和解决方法记录下来。问题1词法分析器无法识别多字符运算符如,。现象输入a b被识别为a,,,b四个Token。原因我们的词法分析器是单字符匹配看到第一个就返回了ASSIGNToken。解决在get_next_token方法中识别单字符后可以“偷看”下一个字符peek。如果下一个字符也是则组合成EQ等于Token。这需要修改advance和peek的逻辑。问题2语法分析器报告“Invalid syntax”但代码看起来没错。排查步骤打印Token流首先将词法分析器输出的Token列表打印出来确认分词是否正确。常见错误是标识符包含了数字、数字识别错误等。检查当前Token在语法分析器每个解析函数的开头和eat调用前打印current_token看程序实际执行到了哪一步在哪里出的错。核对语法规则仔细检查你的BNF规则和递归下降函数是否一一对应。特别是*零次或多次和一次或多次的循环处理部分while循环的条件和结束条件是否正确。问题3生成的代码执行结果不对尤其是运算优先级错误。现象输入3 5 * 2生成代码3 5 * 2但执行结果却是16先算了35。原因这通常不是语法分析器的问题因为我们的AST构建是正确的。问题出在代码生成阶段。如果你在BinOpNode.to_python()中没有为子表达式添加括号而直接拼接为f‘{left_str} {self.op} {right_str}‘那么对于表达式3 5 * 2其AST为BinOp(, Num(3), BinOp(*, Num(5), Num(2)))。递归生成代码时会先得到left_str“3“,right_str“5 * 2“拼接后就是“3 5 * 2“。在Python中这符合优先级没问题。但如果你生成的是某些优先级规则不同的语言或者进行字符串拼接时顺序不对就会出错。解决最稳妥的办法是在BinOpNode.to_python()中无论左右节点是什么都为其加上括号return f‘({left_str} {self.op} {right_str})‘。虽然会产生多余括号如((3) ((5) * (2)))但保证了万无一失。对于三地址码则不存在优先级问题因为每条指令都是原子的。问题4如何处理更复杂的语句比如if或while思路扩展这需要扩展我们的语言定义和编译器。词法分析增加对应的关键字Token如IF,WHILE,ELSE。语法规则在BNF中添加新的规则。例如statement : assign_stmt | if_stmt | while_stmt if_stmt : IF expr ‘{‘ program ‘}‘ (ELSE ‘{‘ program ‘}‘)? while_stmt : WHILE expr ‘{‘ program ‘}‘AST节点创建IfNode和WhileNode包含条件表达式和语句块。代码生成在生成代码时IfNode需要生成条件跳转指令三地址码或if ...:语句Python。这个过程虽然工作量增加但模式是重复的定义Token更新语法规则实现新的AST节点和对应的解析函数最后实现代码生成。这就像搭积木核心框架搭建好后添加新功能是模块化的。给初学者的最终建议不要试图一开始就做一个功能完整的编译器。从我们今天这个只能处理赋值和四则运算的迷你编译器开始每成功添加一个新功能比如支持负数、支持print语句、支持if你都会对编译原理有更深的理解。动手调试观察每一步的中间结果Token流、AST、生成代码是学习这个过程最有效的方式。当你看到自己写的编译器将一段文本成功转换成可以执行的指令时那种成就感是无与伦比的。

相关新闻

2026 无线延长器的核心原理是什么?潜创微专业方案商深度解析

2026 无线延长器的核心原理是什么?潜创微专业方案商深度解析

一、无线延长器的核心原理解无线延长器的核心作用是突破音视频信号的传输距离限制,其技术原理围绕信号转换、稳定传输、还原输出三个核心环节,分为有线和无线两类主流技术路线。(一)有线HDMI延长器原理(以潜创微HDMI网…

2026/6/17 14:27:58阅读更多 →
2026深圳全屋定制避坑指南:花十几万买的教训,为你拆解本地商家的底层虚实

2026深圳全屋定制避坑指南:花十几万买的教训,为你拆解本地商家的底层虚实

选择深圳全屋定制哪家好,核心在于看商家是否具备本地实体制造与全链路交付能力。在深圳本地,真正好的全屋定制必须满足看得到工厂、摸得到展厅、管得了售后的闭环标准,例如像源木匠心这类拥有深圳5000㎡自有工厂与3000㎡实景展厅、总投入超13…

2026/6/17 14:27:58阅读更多 →
XML Notepad完全指南:5分钟掌握微软开源XML编辑神器

XML Notepad完全指南:5分钟掌握微软开源XML编辑神器

XML Notepad完全指南:5分钟掌握微软开源XML编辑神器 【免费下载链接】XmlNotepad XML Notepad provides a simple intuitive User Interface for browsing and editing XML documents. 项目地址: https://gitcode.com/gh_mirrors/xm/XmlNotepad 还在为复杂的…

2026/6/17 14:27:58阅读更多 →
[实战指南] 2026年制造业FAI报告自动生成的全流程解析与实施路径

[实战指南] 2026年制造业FAI报告自动生成的全流程解析与实施路径

在 2026 年的数字化工厂环境中,FAI 报告自动生成(FAI report auto generation)已成为质量管理体系(QMS)中的标配。面对日益复杂的航空航天零件或高精度汽车组件,传统的“手工量取Excel 填报”模式已无法满足…

2026/6/17 15:38:38阅读更多 →
Minecraft服务器性能优化的完整指南:Spark如何快速诊断和修复卡顿问题

Minecraft服务器性能优化的完整指南:Spark如何快速诊断和修复卡顿问题

Minecraft服务器性能优化的完整指南:Spark如何快速诊断和修复卡顿问题 【免费下载链接】spark A performance profiler for Minecraft clients, servers, and proxies. 项目地址: https://gitcode.com/gh_mirrors/spark6/spark 对于Minecraft服务器管理员来说…

2026/6/17 15:38:38阅读更多 →
ZigBee Alarms集群:物联网设备告警标准化与工程实践

ZigBee Alarms集群:物联网设备告警标准化与工程实践

1. 从Level Control到Alarms:理解ZCL集群的协同工作如果你正在开发基于ZigBee的智能设备,比如一个可调光灯具或温控器,你很可能已经和Level Control集群打过交道。它负责处理“亮度从10%平滑过渡到80%”这类指令。但你想过没有,当…

2026/6/17 15:38:38阅读更多 →
void Image Viewer版本演进解析:从1.0.0.15到最新版本的功能变化指南

void Image Viewer版本演进解析:从1.0.0.15到最新版本的功能变化指南

void Image Viewer版本演进解析:从1.0.0.15到最新版本的功能变化指南 【免费下载链接】voidImageViewer Lightweight image viewer for Windows with animated GIF/WEBP support 项目地址: https://gitcode.com/gh_mirrors/vo/voidImageViewer void Image Vi…

2026/6/17 15:38:38阅读更多 →
【合肥经济学院毕业论文】基于SpringBoot的24小时自助民宿管理系统

【合肥经济学院毕业论文】基于SpringBoot的24小时自助民宿管理系统

注:仅展示部分文档内容和系统截图,需要完整的视频、代码、文章和安装调试环境请私信up主。学生的技术与实现摘要伴随着旅游业的发展,民宿作为一种新型住宿方式越来越受到人们的喜爱,在此背景下,民宿主人却面临管理困难…

2026/6/17 15:38:38阅读更多 →
【编码译码】信道编译码Matlab仿真(含RS BCH turbo LDPC RSBCH级联)

【编码译码】信道编译码Matlab仿真(含RS BCH turbo LDPC RSBCH级联)

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、程序设计科研仿真。🍎完整代码获取 定制创新 论文复现点击:Matlab科研工作室👇 关注我领取海量matlab电子书和数学建模资料 &#x1f3…

2026/6/17 15:33:37阅读更多 →
飞书机器人接入 OpenClaw 完整落地部署指南(含安装包)

飞书机器人接入 OpenClaw 完整落地部署指南(含安装包)

OpenClaw 2.7.9 对接飞书机器人完整配置教程 本文讲解借助长连接模式打通 OpenClaw 与飞书的操作流程,配置完成后,可在飞书私聊、群组内发送指令,调用本地 AI 实现电脑自动化操作。整体流程分为飞书平台创建应用、权限配置、密钥填写三大环节…

2026/6/17 10:40:20阅读更多 →
嵌入式处理器技术演进与飞思卡尔实战解析:从架构选型到系统设计

嵌入式处理器技术演进与飞思卡尔实战解析:从架构选型到系统设计

1. 嵌入式处理器:从“大脑”到“神经系统”的进化 在电子设备无处不在的今天,我们很少会去思考一个智能设备是如何“思考”和“行动”的。无论是汽车引擎的精准控制、工厂机械臂的流畅运转,还是智能家居的自动响应,其背后都离不开…

2026/6/17 10:40:20阅读更多 →
如何高效使用BallonTranslator:3分钟完成漫画翻译的完整实用指南

如何高效使用BallonTranslator:3分钟完成漫画翻译的完整实用指南

如何高效使用BallonTranslator:3分钟完成漫画翻译的完整实用指南 【免费下载链接】BallonsTranslator 深度学习辅助漫画翻译工具, 支持一键机翻和简单的图像/文本编辑 | Yet another computer-aided comic/manga translation tool powered by deeplearning 项目地…

2026/6/17 10:40:20阅读更多 →