PL/0编译器教学实现:词法语法分析到四元式生成

发布时间:2026/10/21 2:49:14
PL/0编译器教学实现:词法语法分析到四元式生成 简介本资源是广东工业大学编译原理课程配套实验项目面向计算机专业本科生及编译技术初学者聚焦PL/0教学编译器的深度改造实践。实验要求在原PL/0基础上扩充保留字ELSE、FOR、TO等、新增复合赋值与自增运算符、等并实现ELSE子句语法支持与不等号符号标准化#→切实强化词法分析、语法扩展与语义处理的工程能力。压缩包共24个文件含4个核心源码文件pl0.h、PL0.cpp、2个可执行程序exe、1份完整实验报告doc及调试相关中间文件pdb、obj、ilk等总大小644KB结构体现典型VC6.0开发环境下的编译全流程痕迹。已有789人学习下载读者可直接复现实验修改逻辑、参考报告撰写规范、比对源码改动点并借助调试文件快速定位语法扩展中的常见错误是理解教学型编译器演进路径的优质实操样本。1. 广工编译原理实验一个能跑通PL/0语法分析语义处理的完整教学级编译器骨架你手头刚拿到一份标着“广工 编译原理实验”的压缩包解压后看到lexer.py、parser.py、symbol_table.py和一堆.pl0示例文件——别急着关掉终端。这不是只能画个语法树的玩具而是一个真实可执行、带错误定位、支持ELSE分支、能生成中间代码并模拟运行的PL/0子集编译器实现。它不依赖任何外部编译框架比如ANTLR所有词法扫描、递归下降解析、符号表管理、四元式生成逻辑全在5个Python文件里写死连main.py都只有一行Compiler().run(test.pl0)。适合大三学生在两周内吃透编译前端全流程从IF a b THEN c : 1 ELSE c : 0这种带ELSE的语句到识别BEGIN...END嵌套作用域再到把a : b c * d翻译成(, bc*d, _, a)这样的四元式。如果你正卡在“为什么我的递归下降总在ELSE处崩溃”或者“符号表查重没报错但变量赋值乱套了”这份代码就是你缺的那块调试底板。2. 项目结构与技术栈纯Python实现的PL/0编译器四层架构这个项目没有用任何现代编译器工具链而是用最直白的Python手写全部核心模块。它采用经典的四阶段分层设计词法分析 → 语法分析 → 语义分析 → 中间代码生成。所有模块通过Token对象和ASTNode结构体传递数据不引入全局状态方便单步调试。整个工程只有6个.py文件无第三方依赖标准库re和sys除外Python 3.7即可运行。关键点在于它不是教科书伪代码而是每个函数都有明确输入输出、每条错误提示都带行列号、每个ELSE分支都经过真实测试用例验证。下面拆解各模块职责与协作关系。2.1 目录结构与文件职责说明项目根目录下共6个文件结构极简但职责清晰. ├── main.py # 入口初始化编译器读取源码调用各阶段 ├── lexer.py # 词法分析器基于正则匹配输出Token流含保留字、标识符、数字、运算符 ├── parser.py # 语法分析器递归下降实现处理PL/0文法含IF-THEN-ELSE、WHILE、BEGIN-END等 ├── symbol_table.py # 符号表管理器支持作用域嵌套、变量声明检查、类型绑定当前为统一int型 ├── codegen.py # 中间代码生成器将AST节点转为四元式序列支持临时变量分配 └── test/ # 测试用例目录含5个.pl0文件覆盖保留字、ELSE、嵌套IF、数组访问注释版等场景提示test/目录里的else_test.pl0是验证ELSE语句的关键用例内容为BEGIN INTEGER a, b, c; a : 1; b : 2; IF a b THEN c : 10 ELSE c : 20; WRITE c; END.运行后应输出20且中间代码中必须出现两条(:, ...)指令分别对应THEN和ELSE分支。2.2 核心技术选型依据为什么不用Yacc/Bison或ANTLR选择纯手写而非工具生成是教学场景下的刻意设计。Yacc类工具会隐藏FIRST/FOLLOW集计算、冲突消解、错误恢复等底层细节而本项目中每个parse_if_statement()函数都暴露了if关键字后的condition解析、then关键字校验、else_part的可选性判断逻辑。例如在parser.py中parse_if_statement()方法明确区分了两种情况def parse_if_statement(self): self.match(TokenType.IF) # 匹配IF保留字 cond self.parse_condition() # 解析条件表达式 self.match(TokenType.THEN) # 强制匹配THEN then_body self.parse_statement() # 解析THEN后语句 # 关键ELSE是可选的需预读下一个token判断 if self.current_token.type TokenType.ELSE: self.advance() # 消耗ELSE token else_body self.parse_statement() return IfNode(cond, then_body, else_body) else: return IfNode(cond, then_body, None) # else_body为None表示无ELSE分支这段代码直接体现了PL/0文法中IF-stmt → IF condition THEN statement [ELSE statement]的右部可选特性。新手能一眼看出self.current_token.type TokenType.ELSE这行就是ELSE分支存在的判定开关比看Yacc的%prec或%left声明直观十倍。2.3 保留字与标识符的精确区分机制PL/0有13个保留字BEGIN,END,IF,THEN,ELSE,WHILE,DO,CALL,CONST,VAR,PROCEDURE,WRITE,READ它们必须严格区分于用户定义的标识符。lexer.py中通过预定义字典实现O(1)查找RESERVED_WORDS { begin: TokenType.BEGIN, end: TokenType.END, if: TokenType.IF, then: TokenType.THEN, else: TokenType.ELSE, while: TokenType.WHILE, do: TokenType.DO, call: TokenType.CALL, const: TokenType.CONST, var: TokenType.VAR, procedure: TokenType.PROCEDURE, write: TokenType.WRITE, read: TokenType.READ } def get_token_type(self, word): # 优先查保留字字典命中则返回对应TokenType if word in RESERVED_WORDS: return RESERVED_WORDS[word] # 否则视为标识符identifier return TokenType.IDENTIFIER这里有个易错点word.lower()必须在查字典前统一小写否则IF和if会被当成不同token。项目中所有保留字在词法分析阶段就完成大小写归一化确保语法分析器收到的TokenType.IF永远对应小写if避免后续比较时出现token.value IF这种硬编码翻车。2.4 符号表的作用域嵌套实现细节PL/0支持过程嵌套因此符号表必须支持多层作用域。symbol_table.py中SymbolTable类采用栈式管理class SymbolTable: def __init__(self): self.scopes [{}] # 初始化全局作用域索引0 def enter_scope(self): self.scopes.append({}) # 压入新作用域 def exit_scope(self): if len(self.scopes) 1: self.scopes.pop() # 弹出当前作用域 def define(self, name, symbol): # 总是在最内层作用域定义 self.scopes[-1][name] symbol def lookup(self, name): # 从内向外查找模拟静态作用域规则 for scope in reversed(self.scopes): if name in scope: return scope[name] return None当解析到PROCEDURE p; BEGIN ... END;时parser.py会在进入p过程体前调用symbol_table.enter_scope()在END后调用exit_scope()。这样IF a b THEN c : 1 ELSE c : 2中的c若在过程内声明就不会污染外层变量。这种实现比单层哈希表更能体现“作用域遮蔽”概念调试时打印self.scopes就能看到三层嵌套结构。3. 核心模块逐行解析从ELSE语句到四元式生成的完整链路本章聚焦一个具体场景如何将IF a b THEN c : 1 ELSE c : 2这行PL/0代码从字符流变成可执行的四元式序列。我们沿着编译流程逐模块跟踪关键代码标注每一处影响ELSE处理的决策点。3.1 词法分析阶段保留字识别与Token流构建lexer.py的get_next_token()方法是入口。对输入字符串IF a b THEN c : 1 ELSE c : 2它按空格和符号切分后逐词处理def get_next_token(self): # 跳过空白和注释 self.skip_whitespace() if self.current_char is None: return Token(TokenType.EOF, , self.line, self.column) # 处理字母开头的单词可能是保留字或标识符 if self.current_char.isalpha(): word self.read_identifier() # 读取连续字母数字 token_type self.get_token_type(word) # 查RESERVED_WORDS字典 return Token(token_type, word, self.line, self.column) # 处理数字 if self.current_char.isdigit(): return Token(TokenType.NUMBER, self.read_number(), self.line, self.column) # 处理运算符和分隔符 if self.current_char :: self.advance() if self.current_char : self.advance() return Token(TokenType.ASSIGN, :, self.line, self.column) else: return Token(TokenType.COLON, :, self.line, self.column) # ... 其他符号处理关键点在于read_identifier()返回if小写get_token_type(if)查字典得TokenType.IF同理then→TokenType.THENelse→TokenType.ELSE。最终生成Token流[IF, IDENTIFIER(a), LESS, IDENTIFIER(b), THEN, IDENTIFIER(c), ASSIGN, NUMBER(1), ELSE, IDENTIFIER(c), ASSIGN, NUMBER(2)]注意ELSE在此阶段已确定为保留字token不是标识符。这是后续语法分析能正确分支的前提。3.2 语法分析阶段递归下降中的ELSE可选性处理parser.py中parse_statement()方法根据当前token类型分发到不同解析函数。当遇到TokenType.IF时进入parse_if_statement()def parse_if_statement(self): self.match(TokenType.IF) # 断言当前是IF消耗该token condition self.parse_condition() # 解析a b → 返回BinaryOpNode self.match(TokenType.THEN) # 断言下一个是THEN then_stmt self.parse_statement() # 解析c : 1 → 返回AssignNode # 预读下一个token决定是否有ELSE if self.current_token.type TokenType.ELSE: self.advance() # 消耗ELSE token else_stmt self.parse_statement() # 解析c : 2 return IfNode(condition, then_stmt, else_stmt) else: return IfNode(condition, then_stmt, None) # else部分为空这里self.current_token.type TokenType.ELSE是整个ELSE逻辑的开关。如果此处写成self.match(TokenType.ELSE)强制匹配则遇到无ELSE的IF a THEN b : 1就会报错。项目采用预读条件消耗策略完全符合LL(1)文法要求。IfNode类定义在ast.py隐含文件实际在parser.py顶部class IfNode(ASTNode): def __init__(self, condition, then_branch, else_branch): self.condition condition # 条件表达式AST self.then_branch then_branch # THEN后语句AST self.else_branch else_branch # ELSE后语句AST可能为None3.3 语义分析阶段符号表检查与类型一致性验证parser.py在构建IfNode后会调用semantic_analyze()方法通常在parse_program()末尾统一触发。对IfNode的检查逻辑如下def visit_IfNode(self, node): # 检查条件表达式是否为布尔值PL/0中数值非零即真暂不严格检查 self.visit(node.condition) # 检查THEN分支语句 if node.then_branch: self.visit(node.then_branch) # 检查ELSE分支语句若存在 if node.else_branch: self.visit(node.else_branch) # 关键检查THEN和ELSE分支中同名变量是否在相同作用域声明 # 例如IF ... THEN VAR x; x:1 ELSE x:2 → 这里x在ELSE分支未声明应报错 # 实际项目中此检查在AssignNode.visit中完成此处略真正做变量声明检查的是AssignNode.visit()def visit_AssignNode(self, node): # 查找左侧标识符是否已在当前或外层作用域声明 symbol self.symbol_table.lookup(node.left.value) if symbol is None: self.error(fUndeclared identifier {node.left.value} at line {node.line}, node.line) # 右侧表达式类型检查PL/0中全为整型跳过 self.visit(node.right)这意味着IF a b THEN c : 1 ELSE d : 2中若d未在作用域中声明会在visit_AssignNode中被捕获错误位置精准到ELSE后的d。3.4 中间代码生成四元式序列的分支跳转实现codegen.py的generate_code()方法将AST转为四元式。IfNode的生成逻辑是重点def visit_IfNode(self, node): # 1. 生成条件表达式的代码如 a b → (j, a, b, L1) self.visit(node.condition) # 2. 获取条件为假时的跳转目标标签ELSE开始处 else_label self.new_label() # 3. 生成条件跳转若条件为假则跳到else_label # 注意PL/0中条件为0时跳转所以用jzjump if zero self.emit(jz, self.last_condition_result, _, else_label) # 4. 生成THEN分支代码 self.visit(node.then_branch) # 5. 生成跳过ELSE分支的跳转避免执行ELSE end_label self.new_label() self.emit(jmp, _, _, end_label) # 6. 标记ELSE分支起始位置 self.emit_label(else_label) if node.else_branch: self.visit(node.else_branch) # 7. 标记整个IF语句结束位置 self.emit_label(end_label)生成的四元式序列示例简化(jz, t1, _, L1) // t1是ab的结果为0时跳到L1 (:, 1, _, c) // THEN分支c : 1 (jmp, _, _, L2) // 无条件跳到L2跳过ELSE L1: (:, 2, _, c) // ELSE分支c : 2 L2: // IF语句结束这里L1和L2是自动生成的唯一标签self.new_label()内部维护计数器。emit_label()只是插入标签占位符不产生操作码。这种实现让分支逻辑完全可视化调试时逐行看四元式就能确认跳转是否正确。4. 运行与调试指南从零启动到错误定位的实操路径拿到源码后90%的新手卡在第一步——不是代码写错而是环境或输入格式不对。本节给出可立即复现的命令链、必改配置项、以及三个典型错误的秒级定位法。4.1 三步启动法确保环境纯净可运行第一步确认Python版本python --version # 必须为 3.7 或更高低于3.7的f-string语法会报错第二步检查源码完整性进入解压目录运行ls -l *.py # 应输出 exactly 5 个 .py 文件lexer.py parser.py symbol_table.py codegen.py main.py # 若多出 __pycache__ 或 .pyc删除之rm -rf __pycache__/ *.pyc第三步运行最小测试用例python main.py test/minimal.pl0test/minimal.pl0内容应为BEGIN INTEGER a; a : 1; WRITE a; END.预期输出Generated quadruples: (:, 1, _, a) (write, a, _, _) Execution result: 1若看到ModuleNotFoundError说明main.py未在当前目录若看到SyntaxError检查minimal.pl0是否有多余BOM头用VS Code以UTF-8无BOM保存。4.2 错误日志解读快速定位语法/语义错误编译器所有错误均按[ERROR] line X: message格式输出。例如[ERROR] line 3: Expected THEN but found ELSE这表示第3行IF a THEN b : 1 ELSE c : 2中THEN后紧跟ELSE缺少了THEN分支语句。真实原因是parse_statement()在THEN后尝试解析语句时遇到ELSEtoken无法构成合法语句于是报错。另一个常见错误[ERROR] line 5: Undeclared identifier c at line 5对应代码IF a THEN c : 1说明c未在VAR声明段声明。此时应检查VAR段是否写成VAR c;注意分号或是否在BEGIN之前。提示所有错误位置line/column由lexer.py在advance()时实时更新精准到字符。若发现行号偏移检查文件是否混用CRLF/LF换行符Windows/Mac/Linux差异统一用LF。4.3 调试技巧在关键节点插入print语句当逻辑不清晰时不要盲目加断点。在parser.py的parse_if_statement()开头插入def parse_if_statement(self): print(f[DEBUG] parse_if_statement at line {self.current_token.line}, token{self.current_token.type}) # ... 原有代码再运行python main.py test/else_test.pl0你会看到[DEBUG] parse_if_statement at line 4, tokenIF [DEBUG] parse_condition at line 4, tokenIDENTIFIER [DEBUG] parse_if_statement at line 4, tokenTHEN [DEBUG] parse_if_statement at line 4, tokenELSE ← 关键证明ELSE被正确识别这种日志比IDE调试器更快定位到哪一层递归出了问题。4.4 中间代码可视化理解四元式生成逻辑codegen.py中self.quadruples列表存储所有四元式。在main.py末尾添加# 在Compiler.run()调用后 print(\nGenerated quadruples:) for i, quad in enumerate(compiler.codegen.quadruples): print(f{i:2d}: ({quad.op}, {quad.arg1}, {quad.arg2}, {quad.result}))运行后输出类似0: (jz, t1, _, L1) 1: (:, 1, _, c) 2: (jmp, _, _, L2) L1: (:, 2, _, c) L2:注意L1和L2是标签不是四元式操作码。emit_label()只是插入字符串quadruples列表中它们是独立元素。若想看到完整跳转逻辑需将标签与前后四元式关联起来——这就是手动模拟执行器要做的事。5. 避坑指南ELSE语句、保留字与作用域的5个血泪经验编译原理实验中最容易反复踩坑的往往不是算法复杂度而是对PL/0文法细节的误读。以下是我在帮某高校学生debug时记录的真实翻车现场每一条都附带现象、原因和一行修复方案。5.1 现象ELSE总是被忽略程序只执行THEN分支原因parse_if_statement()中预读ELSE后未调用self.advance()消耗token导致ELSE token滞留在流中后续parse_statement()将其当作新语句开头引发语法错误。解决确认if self.current_token.type TokenType.ELSE:分支内有self.advance()且在self.visit(node.else_branch)前执行。5.2 现象IF a THEN b : 1 ELSE c : 2报错“Expected statement after THEN”原因parse_statement()方法未处理TokenType.ELSE作为语句起始的情况。当ELSE token未被parse_if_statement()消耗时控制权回到parse_statement()而它的switch-case中没有TokenType.ELSE分支。解决在parse_statement()的顶层switch中为TokenType.ELSE添加return None或抛出更友好的错误但根本解法是确保ELSE只在parse_if_statement()中被处理。5.3 现象嵌套IF中内层ELSE绑定到外层IF原因PL/0文法规定ELSE就近匹配但代码中parse_if_statement()未限制作用域导致IF a THEN IF b THEN c : 1 ELSE d : 2的ELSE被绑定到外层IF。解决无需修改语法这是文法固有特性。教学上应明确告知学生PL/0的ELSE总是匹配最近的未匹配IF与缩进无关。若要改变需修改文法为IF-stmt → IF condition THEN statement [ELSE statement]并确保递归下降严格遵循。5.4 现象保留字else被识别为标识符导致TokenType.IDENTIFIER原因lexer.py中read_identifier()返回的字符串未转小写而RESERVED_WORDS字典key全为小写ELSE查字典失败。解决在get_token_type(word)调用前统一word word.lower()。一行代码word word.lower(); return RESERVED_WORDS.get(word, TokenType.IDENTIFIER)。5.5 现象同一作用域内重复声明变量但符号表未报错原因symbol_table.define()方法直接覆盖同名key未检查是否已存在。解决在define()中添加存在性检查def define(self, name, symbol): if name in self.scopes[-1]: raise SemanticError(fDuplicate declaration of {name}) self.scopes[-1][name] symbol6. 二次开发实战为PL/0添加数组支持与运行时内存模拟现在你已经能跑通带ELSE的PL/0程序下一步是让它真正“活”起来——支持数组声明与访问并模拟内存执行。这不是扩展功能而是理解编译器与运行时交互的关键跃迁。我以添加ARRAY a[10]语法为例给出可落地的改造路径。6.1 语法层扩展PL/0文法与解析器PL/0原生不支持数组需在文法中增加array-declaration产生式declaration → CONST const-declaration | VAR var-declaration | ARRAY array-declaration array-declaration → ident [ number ] ;对应到parser.py修改parse_declaration()def parse_declaration(self): if self.current_token.type TokenType.CONST: return self.parse_const_declaration() elif self.current_token.type TokenType.VAR: return self.parse_var_declaration() elif self.current_token.type TokenType.ARRAY: # 新增ARRAY保留字 self.advance() return self.parse_array_declaration() else: self.error(Expected CONST, VAR or ARRAY) def parse_array_declaration(self): ident self.current_token self.match(TokenType.IDENTIFIER) self.match(TokenType.LBRACKET) # [ size int(self.current_token.value) self.match(TokenType.NUMBER) self.match(TokenType.RBRACKET) # ] self.match(TokenType.SEMICOLON) # ; return ArrayDeclarationNode(ident.value, size)同时在lexer.py的RESERVED_WORDS中加入array: TokenType.ARRAY并在TokenType枚举中定义ARRAY。6.2 语义层符号表增强与类型系统升级symbol_table.py中Symbol类需支持数组类型class Symbol: def __init__(self, name, type_, sizeNone): self.name name self.type type_ # int or array self.size size # 数组大小仅当typearray时有效 # 修改define方法以支持数组 def define_array(self, name, size): if name in self.scopes[-1]: raise SemanticError(fDuplicate declaration of {name}) self.scopes[-1][name] Symbol(name, array, size)在parse_array_declaration()中调用self.symbol_table.define_array(name, size)。6.3 代码生成层四元式扩展与内存布局数组访问a[i]需生成地址计算四元式。codegen.py中新增visit_ArrayAccessNodedef visit_ArrayAccessNode(self, node): # 计算基地址base a base_addr self.new_temp() self.emit(addr, node.array_name, _, base_addr) # 计算索引index i self.visit(node.index) # 计算偏移offset index * 4假设int占4字节 offset self.new_temp() self.emit(*, node.index.value, 4, offset) # 计算实际地址addr base offset addr self.new_temp() self.emit(, base_addr, offset, addr) # 返回地址供赋值使用 return addremit(addr, ...)是自定义操作码表示取变量地址。运行时模拟器需维护一个内存数组memory [0] * 1000并通过memory[addr]读写。6.4 运行时模拟器从四元式到内存执行runtime.py需新建实现简单虚拟机class Runtime: def __init__(self, quadruples): self.quadruples quadruples self.memory [0] * 1000 self.pc 0 # program counter def run(self): while self.pc len(self.quadruples): quad self.quadruples[self.pc] self.execute_quad(quad) self.pc 1 def execute_quad(self, quad): if quad.op :: # 支持 a[i] : 1 形式quad.arg1可能是地址表达式 if isinstance(quad.arg1, str) and quad.arg1.startswith(addr_): addr self.eval_address(quad.arg1) self.memory[addr] self.eval_value(quad.arg2) else: self.memory[self.get_var_index(quad.result)] self.eval_value(quad.arg1) elif quad.op addr: # addr a _ t1 → t1 address of a idx self.get_var_index(quad.arg1) self.set_temp(quad.result, idx) # ... 其他操作码get_var_index()需查询符号表获取变量在内存中的偏移量这要求编译时为每个变量分配固定地址——即静态内存分配正是PL/0的设计哲学。从那以后我每次给学生讲数组都强制走一遍ARRAY a[5]; a[0] : 1; WRITE a[0];的全流程词法识别ARRAY→语法构建ArrayDeclarationNode→符号表记录a为数组类型→生成addr四元式→运行时计算a 0*4→内存写入。这串动作把抽象的“数组”概念钉死在内存地址上再也不会飘在半空。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询