资讯动态

PL/0子集编译器课程设计:六模块流水线与验收用例

发布时间:2026/11/6 4:04:14 来源:尧图企业网站定制
简介这份北邮编译原理课程设计资料包面向计算机专业学生及希望动手理解编译器构造的学习者围绕将Pascal高级语言转换为可执行指令的完整流程展开覆盖词法分析、语法分析、语义分析、代码生成以及符号表管理、错误处理和中间代码等实践环节。压缩包共83个文件约123KB以21个h头文件和19个cpp源文件搭建编译器与编辑器的工程骨架并配有8个pas源程序、8个asm汇编样例及bin、rez等测试与资源文件便于对照不同阶段的输入输出。已有1196人学习下载说明其在课程设计场景中具有一定参考价值。读者可从中获得一套较完整的课程设计工程结构结合Pascal与汇编测试用例观察词法单元识别、抽象语法树构造、类型检查及目标代码生成的落地方式也能参考虚拟机和文本编辑相关模块的组织思路为复现实验、完善课设或深入理解编译系统提供可操作的样本。1. 从一个跑得通却算不对的表达式求值说起某高校编译原理课程设计的答辩现场经常出现这样的场面词法、语法都过了一输入23*4屏幕上却打出 20。问题不在这门课难而在词法、语法、语义、中间代码这几段之间的接口没定死各写各的。这套方案把 PL/0 子集编译器拆成词法分析、语法分析、符号表、语义检查、四元式生成、解释执行六段每段给出输入输出约定、核心数据结构和可复现的用例最后落到一张能自评也能被追问的验收表。适合要在两三周内交出一份能演示、能讲清细节的编译原理课程设计的人。2. 需求与源语言定义把课程设计切成六个能独立验收的模块动手写代码之前最该做的一件事是把我的编译器要接受什么样的程序写死。这一步偷懒后面每加一个语法结构词法、语义、代码生成三处都要跟着改越改越乱。我的做法是先定语言子集再定词法规则表最后按依赖关系排出模块交付顺序让每个模块都能单独跑、单独验收。2.1 源语言子集的取舍为什么砍到 PL/0 而不是自己造一门语言课程设计周期通常只有两到四周文法越大语义检查和代码生成的组合越多。自己造一门带数组、结构体、函数重载的语言写到语义分析就会发现每种类型组合都要单独处理时间根本不够。选 PL/0 的扩展子集是性价比最高的它包含常量定义、变量声明、过程声明、赋值、复合语句、条件、循环、输入输出足以覆盖课程要求里的全部知识点又不至于把精力耗在边角语法上。下面这份 EBNF 是可以直接照抄进设计文档的文法定义扩展了read、write和逻辑条件program block . ; block [ const ident number { , ident number } ; ] [ var ident { , ident } ; ] { procedure ident ; block ; } statement ; statement [ assignment | call ident | begin statement { ; statement } end | if condition then statement | while condition do statement | read ( ident ) | write ( ident ) ] ; condition odd expression | expression ( | # | | | | ) expression ; expression [ | - ] term { ( | - ) term } ; term factor { ( * | / ) factor } ; factor ident | number | ( expression ) ; assignment ident : expression ;这份文法有三处刻意的设计。expression / term / factor三层分开是为了把优先级固化在文法结构里而不是在代码里靠一堆if判断谁先算这正是第 1 章那个23*4翻车的根因。不等号用#而不是是为了避免词法阶段把和的两字符匹配搅在一起。statement整个用方括号包起来允许为空是为了让begin end空块和过程体为空这两种情况都能合法通过省掉后面补特例的麻烦。提示文法一旦定稿就冻结写进设计文档并标注版本。后面所有模块的自测用例都以这份文法为准改文法必须同步改用例否则验收时对不上账。2.2 词法规则表种别码先编好后面全都省事词法分析器的输出是 Token 序列Token 的种别码是整个前端共用的一套词汇表。种别码定得粗语法分析里到处是字符串比较定得太细又会写出一堆用不上的枚举。我一般按关键字、标识符、整数、运算符、界符五类来编运算符用语义化的名字而不是符号本身这样语法分析表打印出来人能看懂。类别匹配规则种别码示例备注关键字const var procedure begin end if then while do call read write oddKW_IF、KW_WHILE与标识符同规则匹配后查表判定标识符字母开头后接字母、数字、下划线ID长度上限设 32超长报错无符号整数连续数字NUM不做溢出检查交给运行期运算符: # - * /OP_ASSIGN、OP_LE、OP_ADD两字符算符优先匹配界符( ) , ; .LPAREN、SEMI、DOT.是程序结束标记表里两字符算符优先匹配这条不是可选项。如果按字符从左到右贪心输入ab会被切成a、、、b语法分析直接崩在赋值语句上。这个坑每年都有人踩解决办法是把、、:三个两字符算符放在单词扫描的最前面判断。2.3 六个模块的职责边界与交付顺序模块划分的关键不是数量好看而是每个模块都有明确的输入输出能脱离上下游单独测试。下面这张表是这套方案的模块清单建议按从上到下的顺序交付每一步都能独立演示。模块输入输出独立自测方式依赖词法分析源程序字符串Token 序列打印 Token 流人工比对无语法分析Token 序列抽象语法树打印带缩进的树形结构词法符号表管理声明节点作用域栈与名字条目dump 符号表内容语法树语义检查语法树 符号表类型标注树 错误列表故意写错程序看报错符号表四元式生成类型标注树四元式序列打印四元式手工推演语义解释执行四元式序列输出结果 / 运行期错误跑示例程序对答案四元式顺序不能颠倒。词法的 Token 种别码是语法的输入约定语法的树节点结构是语义检查的输入约定语义检查打上的类型标注又是代码生成的依据。任何一步跳着做最后都要回头返工这是血泪经验。交付节奏上前三个模块建议在一周内跑通因为它们是纯结构转换没有太多设计决策语义和代码生成各留三到四天留一天专门做用例。3. 总体架构设计三段式流水线与一张活的作用域栈整体架构的核心问题是各阶段之间传什么、谁持有全局状态。传得太细模块耦合传得太粗下游拿不到需要的信息。这套方案用三段式流水线中间产物分别是 Token 流、抽象语法树和四元式序列全局状态只有一个符号表和一个错误收集器其余全部通过参数传递。3.1 数据流Token 流、语法树、四元式的三段式衔接第一段是词法到语法。词法分析器一次性把源程序切成 Token 列表末尾追加一个EOF作为哨兵。语法分析只认种别码不认字面量这样后面换一门源语言只需要改词法语法分析器一行不动。Token 里保留行号、列号和原始文本这两个字段在报错时是刚需不保留的话诊断信息只能告诉用户有语法错误验收时会被追问到答不上来。第二段是语法到语义。抽象语法树的每个节点带一个kind字段和若干子节点节点结构要能表达优先级ab*c的根是右孩子是*。树的节点不加类型信息类型检查在语义阶段遍历时填进去。这种先建结构后填属性的做法好处是语法分析和语义检查彻底解耦语法分析器不需要知道int是什么。第三段是语义到执行。四元式是最朴素的三地址码格式为(op, arg1, arg2, result)够用且好调试。生成后直接交给一个解释器执行解释器维护一个运行栈变量按层次和偏移寻址。不生成真正的目标机器码是因为课程设计的验收重点是前端机器码那部分写出来也没人看反而挤占调试时间。三段的衔接处各有一个闸门Token 流末尾的EOF、语法树的根节点、四元式序列末尾的HALT。闸门的实际作用是让每一段都能独立判定输入结束了没有避免下游在空输入上死循环。这个细节听起来微不足道但没有它输入一个空文件就能让程序挂住。3.2 符号表作用域栈与名字解析符号表是整个前端唯一的全局状态设计不好会牵连语义检查和代码生成两个模块。最常见的错误设计是用一个哈希表装所有名字遇到过程嵌套就分不清同名变量属于哪一层语义检查会把内层变量当成外层变量用报出一堆莫名其妙的重复声明。正确的做法是作用域栈每进入一个block压一层离开时弹一层。查找名字时从栈顶往下找找到第一个就返回天然实现了内层遮蔽外层的语义。class SymbolTable: def __init__(self): self.scopes [{}] # 栈底是全局作用域 self.counts [0] # 每层已分配的变量个数用来算偏移 def enter_scope(self): 进入 block / procedure 时调用 self.scopes.append({}) self.counts.append(0) def leave_scope(self): 离开时调用必须保证异常路径上也会走到 self.scopes.pop() self.counts.pop() def declare(self, name, kind, type_None): 在当前层声明名字同层重复声明返回 None 由调用方报错 cur self.scopes[-1] if name in cur: return None self.counts[-1] 1 cur[name] { kind: kind, # const / var / procedure type: type_, # int本方案只有一种类型 level: len(self.scopes) - 1, # 层次代码生成寻址要用 addr: self.counts[-1] - 1, # 层内偏移 } return cur[name] def lookup(self, name): 由内向外查找实现遮蔽语义 for lvl in range(len(self.scopes) - 1, -1, -1): if name in self.scopes[lvl]: return self.scopes[lvl][name] return Nonelevel和addr这两个字段是为解释执行准备的。变量寻址时用(level, addr)二元组运行时栈上按层切帧从当前层往上数level层再取第addr个槽位就能拿到值。如果不存这两个字段运行期就得靠名字查表性能差不说递归调用时还会串值。leave_scope必须放在finally块里调用。这一点在第 5 章还会展开语义检查遇到错误通常要抛异常中断当前 block如果只在正常路径弹栈作用域栈就会和实际嵌套深度对不上后面的名字解析全错。3.3 错误处理把诊断信息当第一类公民课程设计里最容易被扣分的不是功能缺失而是报错信息。一个只打印语法错误的编译器答辩时老师随便输一个错程序你就得现场翻代码找位置。合格的诊断信息至少包含四项错误类别、行号列号、出错附近的原文片段、期望什么。统一成一个错误收集器各阶段往里丢最后一次性输出。这样做的好处是语法分析遇到错误可以跳过继续分析一次报出多个错误而不是报一个就退出。class Diag: 全局错误收集器各阶段共用 def __init__(self): self.items [] def add(self, stage, line, col, code, msg, hint): self.items.append({ stage: stage, # LEX / PARSE / SEMANTIC / RUNTIME line: line, col: col, code: code, # 如 E1001便于统计用例命中 msg: msg, hint: hint, # 期望值、候选修正 }) def report(self): for e in sorted(self.items, keylambda x: (x[line], x[col])): print(f[{e[stage]}] {e[line]}:{e[col]} {e[code]} {e[msg]}) if e[hint]: print(f 提示{e[hint]}) return len(self.items)错误码用E 四位数字的格式词法E10xx、语法E20xx、语义E30xx、运行期E40xx。这套编码在验收时特别好用测试用例表里直接写期望命中 E3001跑一遍对比错误码通过与否一目了然不用人工读报错文本。注意错误收集器不要在若干具体情况下立即终止程序除 EOF 提前出现之外都按记录并恢复处理。恢复策略见 4.2 节。4. 核心模块实现词法状态机、LL(1) 分析表与四元式回填有了接口约定剩下的是每个模块内部怎么写。这一章给四个核心模块的关键代码和设计取舍代码都是能直接跑的最小实现参数含义和改写方式一并说明。4.1 词法分析器最长匹配与状态推进词法分析的本质是一个带最长匹配的状态机。手写扫描器比正则表达式引擎更好调试因为出错时你能在断点里看到当前字符和位置。核心循环按空白跳过 → 注释跳过 → 标识符/关键字 → 数字 → 两字符算符 → 单字符算符 → 界符 → 报错的顺序判断顺序本身就是优先级。KEYWORDS {const, var, procedure, begin, end, if, then, while, do, call, read, write, odd} OP_KIND {: OP_LE, : OP_GE, :: OP_ASSIGN, : OP_ADD, -: OP_SUB, *: OP_MUL, /: OP_DIV, : OP_EQ, #: OP_NE, : OP_LT, : OP_GT} DELIM_KIND {(: LPAREN, ): RPAREN, ,: COMMA, ;: SEMI, .: DOT} def tokenize(src, diag): tokens, i, line, col [], 0, 1, 1 n len(src) while i n: ch src[i] if ch in \t\r: i 1; col 1 continue if ch \n: i 1; line 1; col 1 continue if src.startswith(/*, i): # 块注释不嵌套遇到未闭合直接报错到文件尾 start_line, start_col line, col i 2; col 2 while i n and not src.startswith(*/, i): if src[i] \n: line 1; col 1 else: col 1 i 1 if i n: diag.add(LEX, start_line, start_col, E1001, 注释未闭合) break i 2; col 2 continue # 标识符与关键字同一条规则扫完再查表 if ch.isalpha(): j i while j n and (src[j].isalnum() or src[j] _): j 1 word src[i:j] if len(word) 32: diag.add(LEX, line, col, E1002, f标识符超长 {word}) kind KW_ word.upper() if word in KEYWORDS else ID tokens.append(Token(kind, word, line, col)) col j - i; i j continue if ch.isdigit(): j i while j n and src[j].isdigit(): j 1 tokens.append(Token(NUM, src[i:j], line, col)) col j - i; i j continue # 两字符算符必须先试否则 会被切成 和 two src[i:i 2] if two in OP_KIND: tokens.append(Token(OP_KIND[two], two, line, col)) i 2; col 2 continue if ch in OP_KIND: tokens.append(Token(OP_KIND[ch], ch, line, col)) i 1; col 1 continue if ch in DELIM_KIND: tokens.append(Token(DELIM_KIND[ch], ch, line, col)) i 1; col 1 continue diag.add(LEX, line, col, E1003, f非法字符 {ch!r}) i 1; col 1 # 跳过坏字符继续扫描保证一次报全 tokens.append(Token(EOF, , line, col)) return tokens几点说明。line和col在每条分支里都要同步推进这是最容易漏的地方漏了之后所有报错的行列号全偏。非法字符不抛异常而是记录后跳过是为了让词法阶段一次报出所有坏字符而不是报一个停一次。EOF哨兵带上最后的位置语法分析报文件意外结束时能指到正确位置。写完之后立刻做一件事拿源程序文件逐个打印 Token 流人工核对三五个复杂表达式。这一步花十分钟能省掉后面调试语法分析的两小时。4.2 语法分析递归下降写起来快LL(1) 分析表好答辩两种做法的差别不在于能力而在于答辩时的可解释性。递归下降每个非终结符一个函数代码直观但被问到为什么这里回退或者这个文法是不是 LL(1)就答不上来。LL(1) 分析表由 FIRST/FOLLOW 集机械生成表本身就是证据被追问时可以直接把表打出来说明冲突在哪。我推荐主实现用 LL(1) 预测分析表理由是它能自动检测冲突文法改动能立刻反映在表的结构上。EPS ε def first_of_seq(seq, first_sets, nonterms): 求符号串的 FIRST 集遇到可推出 ε 的非终结符就继续往后看 result set() for sym in seq: if sym in nonterms: result | (first_sets[sym] - {EPS}) if EPS not in first_sets[sym]: return result else: result.add(sym) return result result.add(EPS) # 整串都能推空 return result def build_table(rules, first_sets, follow_sets, nonterms): rules: {A: [产生式右部列表]}返回 {非终结符: {终结符: 产生式}} table {A: {} for A in nonterms} for A, prods in rules.items(): for prod in prods: f first_of_seq(prod, first_sets, nonterms) for t in f - {EPS}: if t in table[A]: raise ValueError(fLL(1) 冲突{A} 在 {t} 上有多条产生式) table[A][t] prod if EPS in f: # A 可推空则跟随 FOLLOW(A) for t in follow_sets[A]: if t in table[A]: raise ValueError(fLL(1) 冲突{A} 在 {t} 上有多条产生式) table[A][t] prod return table def ll1_parse(tokens, table, startprogram, diagNone): 带 panic 模式恢复的预测分析主循环 stack [$, start] # 栈底放结束符 pos 0 while stack: top stack[-1] look tokens[pos].kind if top $ and look EOF: return True if top in table: # 非终结符查表展开 prod table[top].get(look) if prod is None: diag.add(PARSE, tokens[pos].line, tokens[pos].col, E2001, f在 {top} 处遇到意外的 {look}, hintf期望 {sorted(table[top].keys())}) # 恢复跳过当前 token重新观察下一个 pos 1 if pos len(tokens): return False continue stack.pop() for sym in reversed(prod): # 逆序压栈保证从左到右匹配 if sym ! EPS: stack.append(sym) else: # 终结符直接比对 if top look: stack.pop(); pos 1 else: diag.add(PARSE, tokens[pos].line, tokens[pos].col, E2002, f期望 {top}实际 {look}) stack.pop() # 弹出期望项保持在原 token 上重试 return Falsefirst_of_seq里遇到可推空的非终结符继续往后看这个循环是 FIRST 集计算最容易写错的地方。A → B C如果B能推空FIRST(A)必须包含FIRST(C)只算FIRST(B)会让预测表缺项。ll1_parse的恢复策略有两处区别非终结符查表失败时向前跳一个 token因为当前 token 无论如何都接不下去终结符不匹配时只弹期望项不动 token因为可能是栈顶多压了一个符号。这两种恢复方式组合起来一个错语句不会引发后面一连串的连锁报错。提示文法必须消除左递归并且提取公共左因子这两项检查写进设计文档。expression expression term这种直接左递归会让递归下降无限递归也会让 FIRST 集计算陷入死循环。4.3 语义分析声明检查、类型检查、左值校验三件事语义分析遍历语法树顺手把类型标注填回节点同时做三件检查名字是否已声明、赋值目标是否是左值、运算两边类型是否匹配。本方案只有int一种类型类型检查看似多余但把检查框架搭起来答辩时被问如果要加布尔类型怎么办就能给出明确的扩展点。def visit_assign(node, symtab, diag): # 1) 左边必须是可以赋值的对象 if node.left.kind ! ID: diag.add(SEMANTIC, node.line, node.col, E3001, 赋值号左边不是变量) return None entry symtab.lookup(node.left.name) # 2) 名字必须先声明 if entry is None: diag.add(SEMANTIC, node.line, node.col, E3002, f变量 {node.left.name} 未声明) return None if entry[kind] const: diag.add(SEMANTIC, node.line, node.col, E3003, f常量 {node.left.name} 不允许赋值) return None # 3) 右边求值并做类型标注 rhs_type visit_expr(node.right, symtab, diag) if rhs_type is None: return None if rhs_type ! entry[type]: diag.add(SEMANTIC, node.line, node.col, E3004, f类型不匹配{entry[type]} : {rhs_type}) return None node.type entry[type] node.sym entry # 把符号表条目挂到节点上代码生成直接用 return node.typenode.sym entry这一行是关键设计语义阶段把符号表条目直接挂到语法树节点上代码生成阶段不需要再查表直接用level和addr生成四元式。这样做省掉了一次名字解析也避免了生成了内层变量却被解析成外层变量这类问题。变量使用也要检查是否声明这条检查在visit_expr里对ID节点做逻辑和上面第 2 步一样只是报错码换成未声明的标识符。过程调用要额外核对参数个数和类型虽然 PL/0 的过程没有参数但扩展版通常会加检查点提前留好。4.4 中间代码生成四元式与控制流的回填四元式生成里最麻烦的不是算术表达式而是if和while的跳转目标。条件成立时往哪跳、不成立时往哪跳这些目标在生成条件代码的时候还不知道因为后面的代码还没生成。标准解法是回填先占个空位等目标确定后再回来填。class Emitter: def __init__(self): self.quads [] self.tmp_no 0 def new_tmp(self): self.tmp_no 1 return ft{self.tmp_no} # 每次递增避免临时变量重名 def emit(self, op, a1, a2, res): self.quads.append([op, a1, a2, res]) return len(self.quads) - 1 # 返回下标供回填定位 def backpatch(self, holes, target): for idx in holes: self.quads[idx][3] target def gen_condition(node, em): 条件表达式生成 (真出口链, 假出口链)两条链都待回填 t1 gen_expr(node.left, em) t2 gen_expr(node.right, em) truelist, falselist [], [] if node.op in (OP_EQ, OP_NE): em.emit(j node.op[3:].lower(), t1, t2, None) # 条件跳转目标待定 truelist.append(len(em.quads) - 1) elif node.op in (OP_LT, OP_LE, OP_GT, OP_GE): em.emit(j node.op[3:].lower(), t1, t2, None) truelist.append(len(em.quads) - 1) else: # odd x单目判断生成一条条件跳转 em.emit(jodd, t1, None, None) truelist.append(len(em.quads) - 1) falselist.append(em.emit(j, None, None, None)) # 无条件跳过真分支 return truelist, falselist def gen_while(node, em): start len(em.quads) # 条件判断的入口 truelist, falselist gen_condition(node.cond, em) em.backpatch(truelist, len(em.quads)) # 真出口 循环体第一条 gen_statement(node.body, em) em.emit(j, None, None, start) # 循环体结束跳回条件 em.backpatch(falselist, len(em.quads))# 假出口 循环之后new_tmp用一个全局递增计数器而不是在表达式内部局部计数。如果每进入一个子表达式就把临时变量从t1重新数起嵌套表达式的结果会互相覆盖这是排查起来最费劲的一类错误——程序不报错算出来的数字就是不对。backpatch接收的是一组四元式下标把它们的第四段统一改成目标地址。if的处理和while类似只是真出口回填到then分支开头假出口回填到else分支开头两分支执行完都要跳到整个语句之后。回填顺序和链的合并方法是这一类实现的难点建议先在纸上画一遍带下标的位置图再写代码。5. 避坑与排查五个最容易在验收现场翻车的点这些坑几乎每个做编译器课设的人都会撞上一两个区别只是撞的时候是凌晨三点还是答辩前十分钟。下面按现象 → 原因 → 解决写清楚遇到对应现象可以直接对号入座。5.1 错误恢复把分析器送进死循环现象输入1或者少个分号的程序程序既不报错也不退出CPU 跑满。原因预测分析遇到不匹配时弹出栈顶但没推进pos下一轮循环栈顶还是同一个符号、当前 token 还是同一个反复循环。解决给恢复函数加一条硬约束——每轮恢复必须推进pos或减小栈深度两者至少发生一件在循环里加一个计数器连续恢复超过 50 次就判定为失控输出诊断后强制退出。5.2 表达式优先级写错结果差了十万八千里现象23*4输出 20a-b-c算成a-(b-c)。原因把 - * /放在同一个层级用循环处理左结合变成了按顺序从左到右混合计算。解决文法严格分层成expression → term → factor三层每一层的循环只处理本层运算符加法减法一层、乘法除法一层、括号和原子一层优先级由结构保证不靠代码里的判断。5.3 作用域栈泄漏内层变量污染外层现象过程内部定义的变量在过程外部能访问到或者对两个过程中的同名变量报重复声明。原因语义检查块时抛了异常leave_scope没被调用作用域栈只进不出后面所有名字查找都在错误的层上。解决把leave_scope放进try / finally异常路径和正常路径都保证弹栈另外在每次 dump 符号表时打印栈深和语法树嵌套深度比对一旦不一致立刻能定位。5.4 临时变量重名导致嵌套表达式结果被覆盖现象a : (bc)*(de)算出来等于bc或者某个中间值具体哪个不一定。原因临时变量编号在递归下降的每个函数里各自局部计数不同分支生成了同名t1后生成的覆盖了先生成的。解决临时变量编号挂在Emitter实例上全局递增任何位置生成新临时变量都走new_tmp()打印四元式时检查编号唯一性作为一条固定自检项。5.5 测试用例只写正例答辩现场被一个错例问倒现象自己准备的三个程序都跑得通老师现场输一个begin x : 1 endx没声明程序直接崩溃或者打印出x 0。原因开发过程只用正确程序验证错误路径的报错和恢复逻辑从没执行过。解决用例表里正例与错例按 1:1 配比每个错误码至少有一条对应用例跑用例时自动比对错误码而不是人眼看报错文本这样才能批量回归。注意这五条建议在答辩前一周集中过一遍特别是 5.3 和 5.4它们的共同特征是不崩溃但结果错最容易被当成玄学问题耗掉一整晚。6. 验收与答辩用一套分级用例证明编译器是对的到这一步代码基本成型接下来要解决的其实是信任问题老师凭什么相信你的编译器是对的。答案不是我跑过了而是一张分级用例表加一份能现场复现的演示脚本。6.1 分级测试用例矩阵用例按模块分五级每级的通过标准不同。这套矩阵建议直接写进课程设计报告的测试章节答辩时打印出来放在手边。级别覆盖内容典型用例通过标准L1 词法关键字、标识符、注释、非法字符var a_b1;、/*x*/、Token 流正确命中 E1003 且行列号准确L2 语法缺分号、括号不匹配、缺少 thena : 1 b : 2、(12命中 E2001/E2002且只报一条不连锁L3 语义未声明、重复声明、常量赋值x : 1、const c1; c:2分别命中 E3002、E3003L4 代码生成优先级、if/else、while、过程调用23*4、嵌套 if、累加循环四元式序列正确执行结果与手算一致L5 综合完整的可运行程序求最大公约数、冒泡排序、斐波那契前 N 项输出正确无错误诊断L4 这一级是重灾区。优先级用例必须包含23*4、(23)*4、a-b-c、a/(b*c)四个覆盖优先级和结合性if用例要包含无else、有else、if嵌if三种while用例要包含零次循环、一次循环和多次循环三种边界。L5 的三个程序是答辩现场的主打选它们是因为输出可以独立验算老师不用看代码就知道对不对。跑用例的方式建议写成脚本一条命令跑完全部用例并输出通过率。报告里写124 条用例通过 121 条3 条已知限制比写测试充分有说服力得多。6.2 答辩现场的三分钟演示脚本演示顺序决定第一印象。我一般按先错后对、先小后大、先结果后原理来排第一步打开一个故意写错的程序让它一次性报出三个错误——一个词法、一个语法、一个语义指着屏幕说明每个错误码的含义和行列号定位。这一步展示的是诊断能力比展示功能更能体现工程完成度。第二步换一个正确的程序先打印 Token 流再打印四元式序列最后跑解释执行看输出。三步对应三个模块讲的顺序和流水线顺序一致老师能顺着往下问。第三步打开符号表 dump指着level和addr说明作用域嵌套和变量寻址。如果老师问过程调用时变量怎么找这里就是现成的答案。第四步翻到用例矩阵报通过率和已知限制。主动说限制比被问出来强比如本方案不支持数组扩展点在语义分析的visit_expr里加一个分支即可。整个演示控制在三分钟剩下的时间留给追问。追问最多的三个问题是文法是不是 LL(1)错误恢复怎么做的临时变量怎么编号这三个答案在前面几章里都有提前准备一遍就能答得干脆。从那以后我每次带课程设计都强制先写用例表再写分析器——不是因为流程规范而是因为被自己写的死循环卡到凌晨之后才发现能自动跑一遍的用例集才是真正的后悔药。这套方案的模块划分、代码骨架和用例矩阵整理在一起需要的话可以拿去改成自己的版本。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑