语法制导翻译)
简介这份资源是北京交通大学编译原理课程六个核心实验模块的完整源码集合面向计算机科学与技术专业学生及需要系统练习编译器前端开发的开发者。内容覆盖词法分析、递归下降语法分析、LL(1)文法分析、算符优先文法分析、基于SLR(1)分析法的语法制导翻译以及中间代码生成从字符序列到记号流、抽象语法树、语法检查直至语义动作触发与中间代码产出构成一条完整的编译前端技术链路适合课程实验对照、期末复习与自学编译器构造。压缩包共94个文件以33个cpp源文件与29个头文件为主体辅以21个txt测试用例与文法文件、6个makefile构建脚本及少量c文件整体约66KB按Lab01至Lab06分目录组织结构清晰便于逐模块查阅。目前已有92人学习。读者可据此复现各实验的分析流程理解移进-规约冲突处理与语法制导翻译的落地方式并借助现成测试数据快速验证与排错。1. 编译原理实验从零跑通这套北交大源码到底能帮你省多少时间如果你正在上编译原理这门课大概率会遇到一个很现实的问题课本上的 LL(1) 分析表、SLR(1) 项目集规范族、语法制导翻译这些概念看一遍好像懂了真让你从零写一个词法分析器或者递归下降语法分析器又不知道从哪里下手。这套北京交通大学编译原理课程实验的完整源码集合覆盖了词法分析、递归下降语法分析、LL(1) 文法分析、算符优先文法分析、基于 SLR(1) 分析法的语法制导翻译及中间代码生成、编译器前端实现六个核心实验模块正好对应大多数高校编译原理课程实验的完整链路。适合两类人一是正在做课程实验、需要一份能跑通的参考实现来对照调试的本科生二是想快速回顾编译前端各阶段衔接关系、不想从零造轮子的开发者。每个模块独立成文件可以单独编译运行也可以串起来理解从源程序到中间代码的完整流程。2. 词法分析与递归下降语法分析从字符流到语法树的落地路径2.1 词法分析器的核心逻辑与实现方式词法分析是整个编译前端的第一步任务是把源程序的字符流切分成有意义的单词符号Token同时识别关键字、标识符、常数和界符。这套源码里的词法分析模块采用的是经典的有限自动机思路逐字符扫描遇到字母开头就继续读直到非字母数字然后查关键字表决定是关键字还是普通标识符遇到数字就继续读数字和小数点识别整数或浮点数遇到运算符和界符则直接匹配。我一般会先看它的 Token 结构定义因为后面语法分析器要直接消费这个结构。常见做法是用一个结构体或类保存 token 类型和值比如type字段区分关键字、标识符、常数、运算符value字段存原始字符串。这样语法分析阶段只需要判断type和value就能做推导决策。# 词法分析核心扫描循环示意 KEYWORDS {if, else, while, int, float, return} def tokenize(source): tokens [] i 0 while i len(source): ch source[i] if ch.isalpha(): # 标识符或关键字 j i while j len(source) and (source[j].isalnum() or source[j] _): j 1 word source[i:j] if word in KEYWORDS: tokens.append((keyword, word)) else: tokens.append((id, word)) i j elif ch.isdigit(): # 整数或浮点数 j i while j len(source) and (source[j].isdigit() or source[j] .): j 1 tokens.append((num, source[i:j])) i j elif ch in -*/(){};,: tokens.append((op, ch)) i 1 else: i 1 # 跳过空白和换行 return tokens这段代码的关键在于扫描指针i的推进逻辑识别完一个 token 后必须把i跳到 token 末尾的下一个位置否则会死循环。参数方面KEYWORDS集合决定了哪些标识符被提升为关键字实际实验里通常还会加void、for、do等。容易翻车的地方是浮点数识别——如果源程序里出现1.2.3这种非法输入上面的简化逻辑会把它当成一个 num token 吞掉正规做法是在读到第二个小数点时报词法错误。2.2 递归下降语法分析的推导过程与代码结构递归下降分析法是最直观的自顶向下语法分析方法核心思想是给每个非终结符写一个函数函数体内按照产生式右部依次调用其他非终结符对应的函数或匹配终结符。这套源码里的递归下降模块针对的是算术表达式和简单语句通常包含parse_expr、parse_term、parse_factor三层用来处理运算符优先级。# 递归下降分析算术表达式示意 class Parser: def __init__(self, tokens): self.tokens tokens self.pos 0 def peek(self): return self.tokens[self.pos] if self.pos len(self.tokens) else (eof, ) def match(self, expected): tok self.peek() if tok[1] expected: self.pos 1 return tok raise SyntaxError(f期望 {expected}实际 {tok}) def parse_expr(self): node self.parse_term() while self.peek()[1] in (, -): op self.peek()[1] self.pos 1 right self.parse_term() node (op, node, right) return node def parse_term(self): node self.parse_factor() while self.peek()[1] in (*, /): op self.peek()[1] self.pos 1 right self.parse_factor() node (op, node, right) return node def parse_factor(self): tok self.peek() if tok[0] num or tok[0] id: self.pos 1 return tok elif tok[1] (: self.pos 1 node self.parse_expr() self.match()) return node raise SyntaxError(f意外的 token: {tok})这段代码里parse_expr处理加减、parse_term处理乘除、parse_factor处理括号和原子操作数三层嵌套天然实现了优先级。self.pos是全局扫描位置peek只看不前进match匹配成功才前进。递归下降的优点是代码结构和文法产生式几乎一一对应写起来直观缺点是遇到左递归文法必须改写成右递归或消除左递归否则会无限递归。实验里常见的坑是忘记在parse_factor里处理括号后调用match())导致括号不匹配时错误定位不准。3. LL(1) 文法分析与算符优先文法两张分析表怎么建、怎么用3.1 LL(1) 分析表的构造与预测分析流程LL(1) 分析法的核心是构造一张预测分析表表的行是非终结符列是终结符单元格里填产生式。构造过程分三步先求 FIRST 集再求 FOLLOW 集最后根据FIRST(α)和FOLLOW(A)填表。这套源码里的 LL(1) 模块通常会读入一个文法文件自动计算 FIRST 和 FOLLOW然后生成分析表并驱动一个栈式分析器。# 计算 FIRST 集示意 def compute_first(grammar): first {nt: set() for nt in grammar} changed True while changed: changed False for nt, prods in grammar.items(): for prod in prods: if prod[0] not in grammar: # 终结符开头 if prod[0] not in first[nt]: first[nt].add(prod[0]) changed True else: # 非终结符开头 for sym in prod: if sym in grammar: before len(first[nt]) first[nt] | (first[sym] - {ε}) if ε not in first[sym]: break if len(first[nt]) before: break else: first[nt].add(sym) break return first这段代码用迭代法求 FIRST 集changed标记控制循环直到不再变化。参数grammar是一个字典键是非终结符值是产生式列表每个产生式用字符串或列表表示。注意ε的处理如果某个非终结符能推出空串求 FIRST 时要继续看下一个符号。实际实验里最容易翻车的是 FOLLOW 集计算尤其是当产生式右部末尾是非终结符时要把左部的 FOLLOW 加进去很多人漏掉这一步导致分析表填不全。3.2 算符优先分析法的优先关系表与归约过程算符优先分析法利用运算符之间的优先关系来指导归约核心是构造一张优先关系表表里记录、、三种关系。这套源码里的算符优先模块通常先计算 FIRSTVT 和 LASTVT 集然后根据产生式填表最后用一个栈做移进-归约。步骤栈内容当前输入动作1#id id * id #移进2# id id * id #归约 id3# E id * id #移进4# E id * id #移进5# E id* id #归约 id6# E E* id #比较 和 * *移进7# E E *id #移进8# E E * id#归约 id9# E E * E#归约 *10# E E#归约 11# E#接受这张表展示了算符优先分析器处理id id * id的完整过程。关键点在第 6 步栈顶的和输入串当前的*比较优先关系因为*优先级高于所以选择移进而不是归约。算符优先的优点是实现简单、适合表达式分析缺点是只适用于算符优先文法对if-else这种需要上下文判断的结构无能为力。实验里常见的坑是 FIRSTVT 和 LASTVT 集算错导致优先关系表出现空白或冲突分析器遇到某些输入直接卡死。4. SLR(1) 语法制导翻译与中间代码生成从分析栈到四元式4.1 SLR(1) 项目集规范族的构造与冲突消解SLR(1) 是 LR 分析法的一种简化版本核心是构造项目集规范族然后根据 FOLLOW 集解决归约-归约冲突和移进-归约冲突。这套源码里的 SLR(1) 模块通常会先定义文法然后自动生成 LR(0) 项目集、计算 ACTION 表和 GOTO 表最后驱动分析器。# 构造 LR(0) 项目集闭包示意 def closure(items, grammar): result set(items) changed True while changed: changed False for lhs, rhs, dot in list(result): if dot len(rhs) and rhs[dot] in grammar: nt rhs[dot] for prod in grammar[nt]: new_item (nt, tuple(prod), 0) if new_item not in result: result.add(new_item) changed True return result这段代码里items是项目集合每个项目用(左部, 右部, 点位置)表示。closure函数不断展开点后面是非终结符的项目直到不再新增。参数grammar是文法字典。SLR(1) 和 LR(0) 的区别在于归约动作只对 FOLLOW 集里的终结符生效这样能消解一部分冲突。实验里最容易翻车的是项目集编号和 GOTO 表的对应关系如果状态编号错位分析器会在某个输入下走进错误状态报出莫名其妙的语法错误。4.2 语法制导翻译与四元式生成语法制导翻译的核心思想是在语法分析过程中同步执行语义动作生成中间代码。这套源码里的 SLR(1) 模块通常会在归约时执行语义动作把表达式翻译成四元式。四元式格式是(op, arg1, arg2, result)比如a b c会生成(, b, c, t1)和(, t1, _, a)。# 归约时生成四元式示意 quadruples [] temp_count 0 def new_temp(): global temp_count temp_count 1 return ft{temp_count} def gen(op, arg1, arg2, result): quadruples.append((op, arg1, arg2, result)) # 在归约 E - E T 时执行 def reduce_add(left, right): temp new_temp() gen(, left, right, temp) return temp这段代码里new_temp负责生成临时变量名gen把四元式追加到列表。实际实验里语义动作通常和归约动作绑定比如在 SLR(1) 分析器的归约分支里调用对应的语义函数。参数方面arg1和arg2是操作数result是存放结果的变量或临时变量。常见的坑是临时变量命名冲突——如果多个表达式共用同一个临时变量名生成的中间代码会互相覆盖导致最终结果错误。正规做法是用一个全局计数器保证临时变量唯一。5. 编译器前端实现与常见问题排查六个模块怎么串起来5.1 六个实验模块的衔接关系与数据流这套源码的六个模块不是孤立的它们对应编译前端的完整流水线词法分析输出 Token 流递归下降或 LL(1) 或算符优先或 SLR(1) 消费 Token 流做语法分析语法制导翻译在语法分析过程中生成中间代码编译器前端实现则把前面几个阶段串起来形成一个完整的可执行流程。我一般会先跑词法分析确认 Token 流正确再把 Token 流喂给语法分析器确认语法树或分析过程正确最后打开语义动作看四元式生成是否符合预期。模块输入输出依赖词法分析源程序字符串Token 序列无递归下降语法分析Token 序列语法树/求值结果词法分析LL(1) 文法分析文法定义 Token 序列分析过程词法分析算符优先文法分析文法定义 Token 序列归约过程词法分析SLR(1) 语法制导翻译文法定义 Token 序列四元式序列词法分析编译器前端实现源程序字符串四元式序列全部这张表说明了每个模块的输入输出和依赖关系。实际调试时如果最终四元式不对可以逐级回溯先看 Token 流有没有错再看语法分析有没有走错分支最后看语义动作有没有漏执行。5.2 避坑与常见问题排查现象一词法分析器把关键字识别成标识符。原因通常是关键字表没有包含该关键字或者查表逻辑写在了标识符识别之后但没做替换。解决方法是把关键字集合补全并在识别出标识符后立即查表命中则改类型为关键字。现象二递归下降分析器遇到左递归文法无限递归。原因是文法里存在E - E T这样的左递归产生式递归下降会一直调用自己。解决方法是在写文法时消除左递归改成E - T E、E - T E | ε的形式。现象三LL(1) 分析表出现多重入口。原因是文法不是 LL(1) 文法某个单元格里填了多条产生式。解决方法是提取左公因子或消除左递归如果改完还有冲突说明该文法不适合 LL(1)需要换 SLR(1) 或更强大的分析方法。现象四SLR(1) 分析器在某个输入下报语法错误但文法明明是对的。原因通常是 FOLLOW 集算错导致归约动作没有在正确的终结符上触发。解决方法是打印 FOLLOW 集和 ACTION 表逐项核对重点检查产生式右部末尾是非终结符的情况。现象五四元式生成时临时变量互相覆盖。原因是临时变量命名用了固定字符串或者计数器没有全局唯一。解决方法是把临时变量计数器设为全局变量每次生成新临时变量时递增确保名字不重复。提示调试编译原理实验时建议先把每个模块的中间输出打印出来Token 流、分析栈变化、四元式序列都看一眼比直接看最终结果更容易定位问题。6. 进阶用法用这套源码做自动化测试与文法覆盖验证这套源码除了用来做课程实验还有一个很实用的进阶用法把它当成一个编译前端测试平台验证不同文法在不同输入下的行为。我一般会写一个批量测试脚本把多组源程序喂给词法分析器和语法分析器自动比对输出是否符合预期。# 批量测试词法分析器示意 import subprocess test_cases [ (int a 1;, [keyword:int, id:a, op:, num:1, op:;]), (float b 3.14;, [keyword:float, id:b, op:, num:3.14, op:;]), (if (a 0) return a;, [keyword:if, op:(, id:a, op:, num:0, op:), keyword:return, id:a, op:;]), ] for source, expected in test_cases: result subprocess.run( [python, lexer.py, source], capture_outputTrue, textTrue ) actual result.stdout.strip().split(\n) assert actual expected, f失败: {source}\n期望: {expected}\n实际: {actual} print(f通过: {source})这段脚本用subprocess调用词法分析器把输出按行拆分后和预期比对。参数test_cases是测试用例列表每个元素是源程序和期望的 Token 序列。实际使用时可以把lexer.py换成语法分析器或 SLR(1) 分析器验证不同阶段的输出。这种自动化测试的好处是当你修改文法或调整分析表时能快速发现哪些输入的行为变了。另一个进阶用法是文法覆盖验证构造一组能覆盖所有产生式的输入跑一遍分析器看是否每条产生式都被触发过。如果某条产生式从未被触发说明测试用例覆盖不全或者文法里有死代码。我一般会在分析器里加一个计数器每次归约时给对应产生式加一最后打印统计结果。# 产生式覆盖统计示意 production_hits {} def record_reduction(lhs, rhs): key f{lhs} - { .join(rhs)} production_hits[key] production_hits.get(key, 0) 1 # 分析结束后打印 for prod, count in sorted(production_hits.items()): print(f{prod}: {count} 次)这段代码在每次归约时记录产生式命中次数分析结束后打印。如果某条产生式次数为 0说明测试输入没有覆盖到它。参数lhs和rhs分别是产生式左部和右部。实际实验里这种覆盖统计能帮你快速判断文法是否有冗余产生式或者测试用例是否需要补充。从那以后我每次拿到一套编译原理实验源码都强制先跑一遍批量测试和覆盖统计确认每个模块的输入输出边界都摸清楚了再动手改代码。希望帮到你。本文还有配套的精品资源点击获取