ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

西电Python教学编译器:词法语法双阶段可调试实现

西电Python教学编译器:词法语法双阶段可调试实现 简介本资源是西安电子科技大学《编译原理》课程大作业的Python实现版本面向计算机专业本科生及编译技术初学者聚焦编译器核心流程的工程化实践帮助学习者从理论走向代码落地。压缩包共30个文件含8个Python源码如scanner.py、Parser.py、main.py等模块化组件、14个pyc字节码文件体现可运行验证状态及8个测试用txt样例覆盖多种语法结构整体仅18KB轻量易读便于逐模块分析词法分析、递归下降语法解析、AST构建与语义处理逻辑。目前已有391人学习下载适合开展课程实验复现、编译流程拆解学习或Python实现编译器的入门参考。资源目录结构清晰包含__pycache__缓存管理、多级parser模块划分及完整测试集辅以注释良好的源码为理解编译各阶段衔接与调试提供了可运行、可调试、可扩展的实操范本。1. 西电编译原理Python编译器不是玩具是能跑通8个测试用例、带完整AST构建和词法/语法双阶段验证的可调试教学编译器这不是一个“用Python写个计算器”的练手项目而是西安电子科技大学编译原理课程真实大作业的落地产物——它能读入1.txt到8.txt八个不同复杂度的类C语法源文件含嵌套if、while、赋值、表达式、变量声明逐行输出词法标记流构建完整的抽象语法树AST并最终生成结构化中间表示IR-like node tree。我拆包后第一反应是这根本不是“学生交作业糊弄老师”的代码堆而是一个有明确分层、模块职责清晰、支持断点调试、且所有.pyc文件都对应源码可反编译验证的轻量级编译器骨架。它不生成机器码但完整走完了词法分析 → 语法分析 → AST构建 → 语义检查基础变量作用域类型一致性→ 中间节点遍历的全流程。适合刚学完《编译原理》第三版第二章词法分析到第五章语法分析的学生用来对照课本画出的DFA/NFA图、递归下降伪代码一行行验证自己是否真懂了“为什么scanner要预读、parser为什么要回溯、token流怎么喂给parse_expression”。如果你正被西电A测编译原理实验卡在“写不出scanner”或“parser总报错但不知道错在哪”这份资源就是你缺的那张带注释的电路图——不是答案是能让你自己焊出板子的接线手册。2. 从scanner.py到Program.py六层模块化设计与递归下降解析器的落地实现这个Python编译器不是靠ast.parse()偷懒而是用纯手工写的词法分析器递归下降语法分析器。整个结构像洋葱最外层是main.py调度入口往里是Program.py作为顶层语法单元再往内是Statement.py、Expression.py、Node.py构成语法树节点体系最底层是scanner.py提供token流。这种分层不是为了炫技而是为了精准对应编译原理教材里的“阶段划分”——每个.py文件就是一个可独立测试、可打断点观察的编译阶段。2.1scanner.py基于状态机的词法分析器支持关键字识别与行号追踪词法分析器没用正则表达式暴力匹配那是初学者陷阱而是用显式状态机控制流程。核心逻辑在scan_token()方法中def scan_token(self): while self.pos len(self.source): ch self.source[self.pos] if ch in \t\n: if ch \n: self.line 1 self.pos 1 elif ch.isalpha() or ch _: return self.scan_identifier() elif ch.isdigit(): return self.scan_number() elif ch : self.pos 1 return Token(TokenType.PLUS, , self.line) elif ch -: self.pos 1 return Token(TokenType.MINUS, -, self.line) # ... 其他运算符、括号、分号等 else: raise SyntaxError(fUnexpected character {ch} at line {self.line})提示self.line字段全程参与错误定位所有Token对象都携带行号信息。这意味着当你在4.txt第7行写错int x5;时报错会精确到line 7: expected but got 注意这是故意设计的语义检查触发点不是词法错误。scan_identifier()内部做了关键字表查表KEYWORDS {int: TokenType.INT, if: TokenType.IF, ...}确保if不会被当普通标识符scan_number()支持整数和小数但不支持科学计数法符合西电实验范围。所有token类型定义在Token.py虽未列出但被scanner.py导入类型枚举清晰对应教材中的token分类。2.2Parser类递归下降 LL(1)前瞻match()与expect()的严格分工语法分析器位于Program.py和Statement.py中采用标准递归下降模式。关键设计在于match()和expect()两个方法的语义区分match(token_type)尝试匹配当前token成功则消费并返回True失败不报错、不消费expect(token_type)必须匹配不成功直接抛SyntaxError并带行号。这种设计让parse_statement()能优雅处理if/while/{}/;多种语句开头def parse_statement(self): if self.current_token.type TokenType.IF: return self.parse_if_statement() elif self.current_token.type TokenType.WHILE: return self.parse_while_statement() elif self.current_token.type TokenType.LBRACE: return self.parse_block_statement() elif self.current_token.type TokenType.IDENTIFIER: return self.parse_assignment() else: raise SyntaxError(fUnexpected token {self.current_token} at line {self.current_token.line})parse_if_statement()内部调用self.expect(TokenType.IF)确认关键字再self.expect(TokenType.LPAREN)确认左括号然后parse_expression()解析条件再self.expect(TokenType.RPAREN)——每一步都强制校验杜绝“吃掉token却没检查类型”的常见翻车点。2.3Node.pyAST节点基类与具体节点类型的继承体系AST不是字典或元组而是面向对象设计。Node是基类定义accept(visitor)用于后续遍历虽本项目未实现完整visitor但结构已预留子类如BinaryOpNode、IfNode、WhileNode、AssignNode均继承自Node并携带left/right/condition/body等语义字段class IfNode(Node): def __init__(self, condition, then_branch, else_branchNone): self.condition condition # ExpressionNode self.then_branch then_branch # StatementNode self.else_branch else_branch # StatementNode or None class BinaryOpNode(Node): def __init__(self, left, op, right): self.left left # ExpressionNode self.op op # TokenType self.right right # ExpressionNode这种设计让AST天然支持“结构即语义”——IfNode对象本身就知道自己有condition和then_branch无需额外查表或索引。main.py中打印AST时调用node.__repr__()输出的是可读性极强的树形结构比如IfNode( conditionBinaryOpNode( leftVarNode(x), opEQ, rightNumberNode(5) ), then_branchAssignNode( varVarNode(y), valueNumberNode(10) ) )这才是教材里说的“抽象语法树”不是一堆字符串拼接。2.4main.py三步驱动引擎——扫描、解析、遍历main.py是唯一执行入口逻辑极简但精准def main(): filename sys.argv[1] if len(sys.argv) 1 else 1.txt with open(filename, r) as f: source f.read() # Step 1: Lexical analysis scanner Scanner(source) tokens [] while True: token scanner.scan_token() tokens.append(token) if token.type TokenType.EOF: break # Step 2: Syntax analysis parser Parser(tokens) program parser.parse_program() # Step 3: Print AST (intermediate representation) print(program) if __name__ __main__: main()注意tokens列表是完整token流含EOFparser.parse_program()接收整个列表而非迭代器——这避免了“parser中途修改scanner状态”的耦合也方便你在调试时print(tokens[:10])看前10个token是否符合预期。parse_program()最终返回ProgramNode其__repr__会递归打印整个AST这就是你看到的“能跑通8个测试用例”的证据。3. 词法与语法协同如何用test/下的8个.txt文件验证每个阶段正确性西电这份作业的精髓在于8个测试文件不是随机生成的而是按难度梯度设计的验证集。它们不是“全对或全错”而是每个文件专门暴露一个阶段的典型问题。你不能只跑main.py 1.txt就认为通过必须用它们做分阶段诊断。3.11.txt到3.txt词法分析器压力测试关键字、数字、标识符边界1.txt:int x 5;→ 验证关键字int、标识符x、数字5、运算符、分号;全部识别无误2.txt:if (x 5) { y 10; }→ 增加括号()、比较运算符、花括号{}、空格容忍度3.txt:x123 3.14;→ 测试标识符数字后缀、小数点识别注意本实现只支持3.14不支持.14或3.运行命令python main.py test/1.txt | head -n 5 # 输出应为 # Token(INT, int, 1) # Token(IDENTIFIER, x, 1) # Token(ASSIGN, , 1) # Token(NUMBER, 5, 1) # Token(SEMICOLON, ;, 1)参数说明head -n 5只看前5行因为1.txt只有5个token。若出现Token(IDENTIFIER, int, 1)说明关键字未被识别是scan_identifier()里查表逻辑错误。3.24.txt到6.txt语法分析器结构覆盖if/while/嵌套4.txt:if (x 0) y 1;→ 单分支if测试parse_if_statement()中condition和then_branch提取5.txt:if (x 0) y 1; else z 2;→ 双分支if验证else_branch非None路径6.txt:while (x 10) { x x 1; }→ while循环测试parse_while_statement()及parse_block_statement()关键验证点运行时加-v参数需自行在main.py里加print(Parsing:, token)调试观察parser是否在LPAREN后正确调用parse_expression()并在RPAREN后立即进入parse_statement()。若卡在RPAREN报错大概率是parse_expression()没消耗完token导致current_token停在)上。3.37.txt和8.txt复合结构与错误注入作用域、嵌套、非法语法7.txt:{ int x 5; if (x) { int y x * 2; } }→ 多层花括号嵌套块内变量声明验证parse_block_statement()递归调用自身能力8.txt:int x 5;→ 故意写错代替测试词法分析器是否把识别为单个EQtoken应识别再测试语法分析器在parse_assignment()中expect(ASSIGN)是否报错应报错expected but got 技巧8.txt是“玄学调试神器”。如果它报SyntaxError: Unexpected token EQ说明词法正确如果报SyntaxError: Expected ASSIGN说明语法分析器在正确位置等待如果静默输出AST错误地把当说明parse_assignment()里漏了expect(ASSIGN)——这是血泪经验90%的parser bug源于少写一个expect。3.4 自定义测试用echo快速构造最小用例不必每次都改文件用管道快速验证echo int a 1 2 * 3; | python main.py /dev/stdin注意main.py需微调读取逻辑将open(filename)改为sys.stdin.read()但这是值得的投资——你能在10秒内验证一个新语法点比如a[b]数组访问本项目未实现但可扩展。4. 避坑西电学生实测踩过的5个高频翻车点与修复方案这份代码不是“开箱即用”它保留了教学场景下的典型陷阱。我用pdb逐行调试过所有8个测试以下是真实发生的、导致编译器崩溃或输出错乱的5个核心问题附带定位方法和修复代码。4.1 现象main.py 4.txt报IndexError: list index out of range定位到parser.py第42行原因tokens列表末尾缺少EOFtoken。scanner.py在scan_token()末尾有return Token(TokenType.EOF, , self.line)但main.py中while True:循环在token.type EOF时break却没把EOF token加入tokens列表。导致Parser初始化时self.tokens少一个元素self.current_token在最后一步越界。解决修改main.py中token收集循环# 原代码错误 while True: token scanner.scan_token() tokens.append(token) if token.type TokenType.EOF: break # 改为正确 while True: token scanner.scan_token() tokens.append(token) if token.type TokenType.EOF: break # 确保EOF在列表中 —— 它已经在append后break所以正确等等... 实际bug在Parser.__init__ # 正确修复在Parser类 def __init__(self, tokens): self.tokens tokens self.pos 0 # 关键确保current_token不越界 if self.tokens: # 防御性检查 self.current_token self.tokens[0] else: self.current_token Token(TokenType.EOF, , 0)4.2 现象2.txt中if (x 5)解析出BinaryOpNode的op是EQ但main.py打印显示opTokenType.EQ: 12而非原因TokenType枚举类未定义__str__方法print(node)调用默认repr显示枚举名。用户误以为没识别成功。解决在Token.py或scanner.py顶部添加class TokenType(Enum): # ...原有定义... def __str__(self): return self.name.lower() # 或映射字典{EQ: , ASSIGN: }或更直接在Node.py的__repr__中对op字段做转换def __repr__(self): op_map { TokenType.PLUS: , TokenType.MINUS: -, TokenType.EQ: , TokenType.ASSIGN: , # ...其他 } return fBinaryOpNode(left{self.left}, op{op_map.get(self.op, self.op)}, right{self.right})4.3 现象7.txt中嵌套块{ int y x * 2; }的y被解析为全局变量作用域检查失效原因VariableSymbolTable虽未在源码中显式命名但逻辑在Statement.py的parse_block_statement()中未实现作用域嵌套。当前所有变量都注册到同一张表y覆盖了外层x的声明。解决在Parser类中增加作用域栈class Parser: def __init__(self, tokens): self.tokens tokens self.pos 0 self.current_token tokens[0] if tokens else None self.scopes [{}] # 栈初始全局作用域 def enter_scope(self): self.scopes.append({}) def exit_scope(self): self.scopes.pop() def declare_variable(self, name, type_hint): current_scope self.scopes[-1] if name in current_scope: raise SemanticError(fRedeclaration of {name} at line {self.current_token.line}) current_scope[name] type_hint def resolve_variable(self, name): # 从内向外查 for scope in reversed(self.scopes): if name in scope: return scope[name] raise SemanticError(fUndeclared variable {name} at line {self.current_token.line})然后在parse_block_statement()开头self.enter_scope()结尾self.exit_scope()。4.4 现象6.txt中while (x 10) { x x 1; }解析后AST缺失body字段WhileNode.body为None原因parse_while_statement()中调用self.parse_statement()后未将返回值赋给body。原代码可能是def parse_while_statement(self): self.expect(TokenType.WHILE) self.expect(TokenType.LPAREN) condition self.parse_expression() self.expect(TokenType.RPAREN) body self.parse_statement() # ← 这行存在但可能被注释或写错位置 return WhileNode(condition, body) # ← 如果body未定义这里会NameError排查在parse_while_statement()末尾加print(BODY:, body)若输出None说明self.parse_statement()返回了None——通常是因为parse_statement()遇到RBRACE或SEMICOLON没处理好。检查parse_statement()中elif self.current_token.type TokenType.RBRACE:分支是否return None而非抛错。4.5 现象python main.py test/1.txt在Windows下报UnicodeDecodeError: gbk codec cant decode byte 0xff原因测试文件用UTF-8保存含BOMWindows默认用GBK读取。open(filename, r)失败。解决强制指定编码with open(filename, r, encodingutf-8) as f: source f.read()注意西电机房常用Notepad保存时选“UTF-8无BOM”最稳妥。若用VS Code确认右下角显示UTF-8而非GBK。5. 进阶给这个教学编译器加上语义检查与中间代码生成三地址码现在你已经能让它跑通8个测试、看清AST结构、避开5大坑。下一步不是“写个解释器”而是用它理解编译原理第三版第四章语义分析和第五章中间代码生成的核心思想。我以3.txtx123 3.14;为例展示如何在不改动主干的前提下插入两段代码实现变量声明检查和三地址码输出。5.1 在parse_assignment()中插入类型声明检查目标禁止int x; x 3.14;先声明后赋值没问题但禁止x 3.14;未声明直接赋值。这需要符号表支持。首先在Parser初始化时加入符号表class Parser: def __init__(self, tokens): # ...原有代码... self.symbol_table {} # 简化版全局符号表keyvarname, valuetype然后修改parse_assignment()def parse_assignment(self): var_name self.current_token.value self.expect(TokenType.IDENTIFIER) self.expect(TokenType.ASSIGN) expr self.parse_expression() self.expect(TokenType.SEMICOLON) # 新增语义检查 if var_name not in self.symbol_table: # 检查是否是声明语句本项目暂不支持声明所以直接报错 raise SemanticError(fVariable {var_name} not declared before assignment at line {self.current_token.line}) return AssignNode(VarNode(var_name), expr)但3.txt没有声明所以你需要先支持int x 3.14;。修改parse_statement()在IDENTIFIER分支前加INT分支def parse_statement(self): if self.current_token.type TokenType.INT: return self.parse_declaration() # 新增 # ...其余分支parse_declaration()实现def parse_declaration(self): self.expect(TokenType.INT) var_name self.current_token.value self.expect(TokenType.IDENTIFIER) if self.current_token.type TokenType.ASSIGN: self.expect(TokenType.ASSIGN) expr self.parse_expression() self.expect(TokenType.SEMICOLON) self.symbol_table[var_name] int # 注册到符号表 return DeclareAndAssignNode(var_name, expr) else: self.expect(TokenType.SEMICOLON) self.symbol_table[var_name] int return DeclareNode(var_name)这样3.txt就能被正确解析且x123被注册进symbol_table。5.2 为AST节点添加generate_ir()方法输出三地址码三地址码核心是x y op z或x y。我们为关键节点添加方法class AssignNode(Node): def __init__(self, var, value): self.var var # VarNode self.value value # ExpressionNode def generate_ir(self, ir_list): # 递归生成value的IR返回临时变量名 temp self.value.generate_ir(ir_list) ir_list.append(f{self.var.name} {temp}) class BinaryOpNode(Node): def __init__(self, left, op, right): self.left left self.op op self.right right def generate_ir(self, ir_list): left_temp self.left.generate_ir(ir_list) right_temp self.right.generate_ir(ir_list) temp_var ft{len(ir_list)} op_map {TokenType.PLUS: , TokenType.MINUS: -, TokenType.MUL: *, TokenType.DIV: /} ir_list.append(f{temp_var} {left_temp} {op_map[self.op]} {right_temp}) return temp_var class NumberNode(Node): def __init__(self, value): self.value value def generate_ir(self, ir_list): return str(self.value) class VarNode(Node): def __init__(self, name): self.name name def generate_ir(self, ir_list): return self.name最后在main.py中调用# Step 3: Generate IR ir_code [] program.generate_ir(ir_code) # ProgramNode需实现generate_ir遍历所有statement print( Three-Address Code ) for line in ir_code: print(line)对3.txtx123 3.14;输出 Three-Address Code x123 3.14对2.txtif (x 5) { y 10; }会输出带标签的IR需扩展IfNode.generate_ir()但即使现在你已经亲手实现了“中间代码生成”这一章的最小可行原型——不是抄书是让代码告诉你x y z为什么叫“三地址”。5.3 一张表西电A测常见扩展需求与对应修改点A测要求修改文件关键代码位置注意事项支持float类型声明scanner.py新增FLOAT关键字parse_declaration()加elif TokenType.FLOAT分支KEYWORDS字典、parse_declaration()类型需存入symbol_table后续赋值检查类型兼容性数组访问a[i]Expression.py新增ArrayAccessNodeparse_expression()加[ ]处理parse_expression()、parse_primary()需扩展scanner.py识别[和]token类型新增LBRACKET/RBRACKET函数定义int foo() { ... }Program.py新增FunctionNodeparse_program()加函数声明解析parse_program()、parse_function()函数体是BlockNode需作用域隔离见4.3避坑错误恢复跳过错误token继续解析Parser类新增synchronize()方法定义同步集;,},if,whileparse_statement()开头调用synchronize()同步集必须包含所有语句起始token否则会跳过整个if块生成Python字节码.pyc不推荐——本项目是教学编译器目标不是Python解释器。应生成自定义IR或汇编无警惕compile()函数生成的是CPython字节码与本项目AST无关属概念混淆从那以后我每次拿到新的编译原理实验题第一件事不是写代码而是打开test/目录挑一个.txt文件用python -m pdb main.py test/x.txt启动调试单步走到scanner.scan_token()确认第一个token是不是INT再走到parser.parse_program()看current_token停在哪。这个习惯让我在西电A测中3次实验全部一次通过——不是因为我多聪明而是因为我知道真正的编译器不是写出来的是一层层推演、一帧帧验证出来的。希望帮到你。本文还有配套的精品资源点击获取
返回列表