简介面向编译原理课程实验与期末报告的参考文档内容围绕“无符号数词法分析程序”展开包含实验目的、文法规则、程序流程图、Java实现代码与运行结果分析。文档以无符号数识别为主线系统梳理词法分析的基本思想、无符号整数与实数的文法定义、比例因子处理逻辑等关键知识点适合计算机相关专业学生在完成编译原理实验或复习词法分析模块时对照参考。资源为单个PDF文件大小约201KB便于下载后直接阅读或打印。目前已有1562人浏览学习适用于从入门到进阶的编译原理学习者。通过该文档可掌握词法分析程序从规则设计到编码实现的全过程理解输入字符流识别、数字与小数点及指数部分的判别思路同时可直接借鉴实验报告的结构组织与代码写法提高实验报告撰写效率。1. 先说一个反直觉的结论编译原理不是“背”出来的是“写”出来的不少人在拿到《编译原理-太原理工大学[参照].pdf》这份资料后第一反应是把它当作考前突击的背诵手册——词法分析、语法分析、语义分析、中间代码生成每章背几个概念做几道清华大学出版社第三版的课后题就觉得这门课稳了。等到实验课要自己写一个词法分析器或者给一个微型语言做递归下降解析才发现课本上的定义和工程能跑通的代码之间隔着一条巨大的鸿沟。这里的核心问题在于编译原理是一门“理论先行、实践验证”的课文法和自动机是工具不是目的。你真正要掌握的能力是——拿到一段字符串能设计出有限自动机去识别它拿到一个文法能构造出预测分析表去解析它拿到一个表达式能生成三地址码去描述它。太原理工大学的这份参照PDF之所以叫“参照”就是因为它是教学的纲领性文件告诉你这门课覆盖哪些知识点、实验要求达到什么程度但它不会替你做实验。这篇笔记的任务就是顺着这份PDF的骨架把它涉及的理论点逐个落地成能复现的操作步骤该学什么、该怎么练、实验怎么做、参数怎么调、坑在哪里。不管你是刚开课的大三学生还是准备期末突击的考研党按下面的路线走一遍比你把PDF翻三遍都管用。2. 这份“参照PDF”到底在讲什么从词法到代码生成的完整骨架2.1 词法分析正规式、NFA/DFA与最小化把“字符串”变成“单词”词法分析是编译器的第一个阶段也是大多数实验课的起点。它的任务简单到可以用一句话描述读入源程序的字符流识别出一个个具有独立含义的单词符号关键字、标识符、常数、运算符、界符并把它们归类、编码、输出。但这句话背后藏着三个必须动手才算掌握的知识点第一个是正规式到NFA的转换。正规式是描述单词结构的工具比穷举字符串集合优雅得多。比如标识符的正规式是letter(letter|digit)*你要能画出对应的NFA状态图理解*意味着可以回到前一状态继续读入字符。实际做实验时绝大多数人不是不会画图而是不会把图画成状态转移表——二维数组的行是状态列是输入符号值是下一状态。这一步跨不过去后面写代码无从谈起。第二个是NFA到DFA的确定化即子集构造法。NFA允许一个状态在某输入符号下转移到多个状态这在工程实现里没法直接用必须把状态的集合作为DFA的单个状态。算法核心是求ε-closure空串闭包和move读入符号后的可达状态集合。做这一步实验时建议先用小例子手算比如识别a(a|b)*b的NFA画出完整的子集构造过程再对比课本答案。手算三遍以上你对“状态集合”这个概念才会有肌肉记忆。第三个是DFA的最小化即分割法。课本上的算法是先把状态分为接受状态和非接受状态两组然后反复检查每个状态组在某个输入符号下是否转移到同一组若不一致则分割。这个算法的执行过程非常机械非常适合用代码实现也适合出考题——太原理工大学的期末卷子喜欢考“最小化前后状态数对比”本质上是在考察你对等价状态的理解。写词法分析器时有一个容易被忽略的工程细节最长匹配原则。比如输入ifx如果词法规则里既有关键字if又有标识符规则那么ifx应该被识别为标识符而不是关键字if后面跟着x。实现上每次识别单词时扫描指针要一直向前推进到无法继续匹配为止记录最后一个接受状态的位置然后回退到这个位置。这个处理不到位后面所有语法阶段都会跟着错。2.2 语法分析从上下文无关文法到LL(1)与LR(1)为什么它是最难啃的一章语法分析是编译原理的核心章节也是最劝退的章节。它的任务是根据词法分析产出的单词序列按照文法规则判断句子是否符合语法并构建语法树。这章的知识密度极高上下文无关文法、最左推导与最右推导、二义性、LL(1)文法、递归下降分析、LR(0)/SLR(1)/LR(1)/LALR(1)分析表、移进-归约冲突与展望信息。很多人在这一章翻车原因是学习方法错了——试图直接背诵各种复杂文法和分析表的构造步骤而不理解它们是在解决什么问题。先看自顶向下分析。LL(1)的意思是从左到右扫描输入L最左推导L向前看一个符号1即可确定用哪个产生式。要构造LL(1)分析表必须会算三个集合FIRST、FOLLOW、SELECT。这三个集合是死套路但太原理工大学的作业题喜欢出“给定文法计算FIRST和FOLLOW集”因为算得对不对直接暴露你是否理解了推导过程。算FIRST时要注意如果产生式右部以终结符开头直接加入如果以非终结符开头递归加入该非终结符的FIRST如果非终结符能推导出空串还要继续看下一个符号。FOLLOW的起点是开始符号的FOLLOW里要加入结束符#这个细节十个人里有三个会漏。再看自底向上分析。LR(1) 系列的难点在于构造项目集规范族。一个项目是文法产生式带上圆点标记比如E → E·T表示已经识别了E期望看到。构造项目集时闭包计算和转移函数的两步操作必须完全机械化。做实验时我一般建议用表格记录左边是项目集编号中间是读入符号后的转移右边是该项目集包含的所有项目。表格一旦分层画清楚基本不会乱。这章的真正难点是两类冲突的解决。移进-归约冲突shift/reduce conflict和归约-归约冲突reduce/reduce conflict是SLR(1)分析表构造失败的根本原因。考试和实验里常见的操作是给你一个有冲突的文法要求你通过添加展望信息改造成LR(1)文法或者直接判定它不是任何LR类文法。做判断时最快的方法不是画完整分析表而是看文法里是否存在左递归或公共左因子——这两种结构在自底向上分析中极易引发歧义。2.3 语法制导翻译与中间代码三地址码和符号表编译器的“胶水层”语法分析只解决“句子对不对”的问题接下来要把“对”的句子翻译成中间表示这就是语法制导翻译要做的事。这一章的实践性极强也是真正区分“背过书”和“懂编译”的分水岭。核心概念是语法制导定义SDD和语法制导翻译方案SDT。SDD 是文法产生式关联属性规则的集合属性分为综合属性和继承属性。综合属性在自底向上分析中自然计算——归约时从子节点求父节点继承属性在自顶向下分析中更自然——推导时从父节点传给子节点。考试和实验里最常见的题目是对表达式2 * 3 4写出带属性注释的分析树。动手做一遍你才会明白为什么E → ET的E.val E1.val T.val这条规则必须写在归约动作里。三地址码是中间代码最常用的形式。每条三地址码最多包含三个运算分量两个操作数一个结果例如t1 2 * 3和t2 t1 4。翻译表达式到三地址码的关键是为临时变量编号这需要在语法制导动作里维护一个变量计数器。实验时最常见的错误是忘记按运算优先级生成临时变量导致生成的代码顺序与原表达式语义不一致。检验方法很简单把生成的三地址码用简单的后续代码模拟执行一遍对比结果就知道有没有错。符号表的管理在这一章开始变得重要。符号表不是“一个数组存变量名”那么简单它需要管理作用域——内层作用域的变量声明不能污染外层同名变量在不同作用域内指向不同条目。常见实现是作用域栈scope stack进入一个块时压栈一个新表退出时弹栈。查名字时从栈顶向下逐个表查找。TS说过“变量声明全串了”的坑九成是作用域栈的入栈出栈时机设错了。用太原理工大学的真题风格来检测你的掌握程度给定一段 C 语言片段int a; { int a; a 1; } a 2;画出符号表的入栈出栈过程以及每次赋值时a对应哪个符号表条目。这个题能全对符号表这块就算过关了。2.4 代码优化与目标代码生成PDF最后几章为什么决定你的上限前几章是编译器的“主体骨架”但如果这门课只讲前半部分它本质上还是“翻译器”而不是“编译器”。代码优化和目标代码生成才是让编译器产生高质量目标代码的临门一脚。代码优化的常规手段包括常量折叠2 3直接算成5、公共子表达式消除同一表达式计算两次只算一次并存临时变量、复写传播a b; …用a的地方直接换成b、死代码消除计算结果不被引用的语句删除。这些优化在基本块内做叫局部优化在基本块之间做叫全局优化。考试里最常见的题型是对给定的三地址码序列找出基本块画出流图标出公共子表达式然后写出优化后的三地址码序列。做这类题有一个定型化的步骤先把三地址码按基本块划分领头语句是某个跳转目标的语句或紧跟在跳转后的语句然后在每个基本块内扫描每条语句查该运算是否已存在两个操作数和运算符完全相同。这个扫描可以用一个哈希表实现键是(运算符, 左操作数, 右操作数)值是最新一次赋值的临时变量。实现完记得做考题反推优化前后的语句数对比不超过20%说明你把优化做的太保守了超过50%要检查是否错误地消除了一个有副作用的函数调用。目标代码生成这章核心是寄存器分配和指令选择。太原理工大学的实验不会要求你生成真实的汇编代码但会要求你掌握简单的寄存器分配算法——比如基于活跃性分析的最简单策略给每个基本块维护活跃变量集合优先把不活跃变量的寄存器让给新变量。这里的“活跃性分析”是反向数据流分析从基本块末尾向前扫描变量被引用则标记活跃被定义则赋值为不活跃。反向扫描很多人第一次做会搞反方向记住口诀活跃性看“引用在前、定义在后”。PDF的最后部分如果提到中间代码的树结构表示和有向无环图DAG别跳过。DAG 是压缩局部冗余的好工具同一个表达式子树只构建一次。笔试中画表达式a b * (a b)的 DAG 是经典送分题但很多人在合并公共子表达式时把结点标号搞错白白丢分。做法是把每个运算结点用(运算符, 左子编号, 右子编号)三元组做键建表查重重复的引用同一个编号。3. 把PDF变成能力一份按周执行的“预习→复现→实验”路线3.1 先定骨架这份PDF对应什么教材和章节重点太原理工大学的编译原理课程通常配套清华大学出版社第三版教材就是热词里反复出现的《编译原理清华大学出版社第三版》这份PDF作为教学参照章节顺序大体是引论→词法分析→语法分析→语法制导翻译→中间代码生成→代码优化→目标代码生成。拿到PDF后第一件事不是从头看而是先看它的目录和每一章的“学习要求”。这些要求往往直接回应考试范围和实验深度。举例来说如果某一章的参照要求写着“掌握正规式、有限自动机及其相互转换”那么考试大概率会出一道10到15分的构造题实验大概率要求你用程序实现一个DFA。如果写着“了解全局优化”那么考试只会出概念题或选择题不必去死磕复杂的算法细节。我的建议是用一张纸记录章节映射表。左边列PDF的章节标题右边列教材对应章节和课后题号再标一列“考试权重高/中/低”和“实验关联有/无”。做完这个动作你对课程重心会有一个全局判断不会再把时间平均分配。3.2 用Python写一个最小词法分析器跟着PDF第二章的算法复现DFA有了理论地基就可以进入动手环节了。词法分析器是第一个值得完整实现的实验不建议直接用Lex或Flex工具一把梭而是先用普通代码结合“状态转移表”实现一次因为你只有手动实现过一次DFA模拟器才能真正看懂工具生成的代码在做什么。下面是一个支持识别标识符、无符号整数、运算符和界符的最小词法分析器骨架用纯 Python 实现模拟的是 DFA 的状态转移过程import re # 状态转移表行 当前状态列 字符类别值 下一状态 # 字符类别0字母1数字2运算符3界符4空格/换行5其他非法 # 状态含义0初始1标识符2整数3运算符4结束-1出错 TRANS [ # 0 1 2 3 4 5 [ 1, 2, 3, 0, 0, -1], # 状态0 [ 1, 1, -1, -1, -1, -1], # 状态1标识符态数字也可以继续 [ -1, 2, -1, -1, -1, -1], # 状态2整数态 [ -1, -1, -1, -1, -1, -1], # 状态3运算符态这里简单处理为单字符 ] # 接受的终止状态1标识符2整数3运算符 ACCEPT {1: IDENT, 2: INT, 3: OP} def classify_char(c): if c.isalpha() or c _: return 0 if c.isdigit(): return 1 if c in -*/!: return 2 if c in (){};,[]: return 3 if c in \t\n: return 4 return 5 def tokenize(source): tokens [] i 0 n len(source) while i n: # 跳过空白 if classify_char(source[i]) 4: i 1 continue state 0 last_accept (None, None) # (位置, 状态) j i while j n: ctype classify_char(source[j]) if state in TRANS and 0 ctype len(TRANS[state]): state TRANS[state][ctype] else: state -1 if state in ACCEPT: last_accept (j 1, state) elif state -1: break j 1 if last_accept[0] is None: # 无法识别跳过该字符实际编译器必须报错 i 1 continue end_pos, acc_state last_accept lexeme source[i:end_pos] tokens.append((ACCEPT[acc_state], lexeme)) i end_pos return tokens if __name__ __main__: src x 10 abc; for tok in tokenize(src): print(tok)这段代码最核心的是两个机制状态转移表驱动和最长匹配回退。转移表决定了一个状态在读入某类字符后跳到哪个状态所有的识别逻辑都浓缩在数组里而不是散落在 if-else 中最长匹配体现在last_accept变量——每当进入接受状态就记录下来扫描继续向前直到走到死路-1状态才停止最终回退到最后一个接受位置。这意味着abc123会被整体识别为一个标识符而不是先识别abc再识别123。写完后一定要跑这几个测试用例ifx验证关键字与标识符的最长匹配、x12验证标识符后的数字、a-b验证连续运算符是否能正确切分。教材配套的第三章课后题里有一道“将以下正规式转换为DFA”的题建议转换成转移表后用这个模拟器验证识别结果是否一致。转移表本身就是DFA的另一种表达形式能把转移表和状态图画到一一对应这一章的基础就真的打牢了。3.3 手写递归下降分析器把LL(1)变成能跑的代码词法分析器产出的 token 流要交给语法分析器。这里给出一个最易上手的递归下降分析器示例它能解析一个包含变量声明和赋值语句的极简语言。递归下降的本质每个非终结符对应一个函数函数体里按产生式右部的顺序调用其他函数或匹配终结符遇到冲突时尝试回溯。以下代码实现了下面这个极简文法的部分功能program → stmt_list stmt_list → stmt { stmt } stmt → assign assign → ID expr ; expr → term { (|-) term } term → factor { (*|/) factor } factor → ID | NUM | ( expr )class RecursiveDescentParser: def __init__(self, tokens): self.tokens tokens self.pos 0 self.errors [] def peek(self): return self.tokens[self.pos] if self.pos len(self.tokens) else None def match(self, expected_type): token self.peek() if token and token[0] expected_type: self.pos 1 return token else: self.errors.append(f位置{self.pos}: 期望 {expected_type}, 实际 {token}) return None def parse_program(self): while self.pos len(self.tokens): self.parse_stmt() if self.errors: print(解析失败) for e in self.errors: print(e) else: print(解析成功) def parse_stmt(self): self.parse_assign() def parse_assign(self): var_tok self.match(IDENT) if var_tok: op_tok self.match(OP) if op_tok and op_tok[1] : self.parse_expr() self.match(DELIM) # 分号 def parse_expr(self): self.parse_term() while self.peek() and self.peek()[0] OP and self.peek()[1] in (, -): self.pos 1 self.parse_term() def parse_term(self): self.parse_factor() while self.peek() and self.peek()[0] OP and self.peek()[1] in (*, /): self.pos 1 self.parse_factor() def parse_factor(self): token self.peek() if token and token[0] in (IDENT, INT): self.pos 1 elif token and token[0] DELIM and token[1] (: self.pos 1 self.parse_expr() self.match(DELIM) # ) else: self.errors.append(f位置{self.pos}: 无法解析的因子 {token}) if __name__ __main__: tokens [ (IDENT, a), (OP, ), (INT, 10), (DELIM, ;), (IDENT, b), (OP, ), (IDENT, a), (OP, ), (INT, 2), (DELIM, ;) ] parser RecursiveDescentParser(tokens) parser.parse_program()这段代码最值得注意的地方是while循环处理左递归的消除。原文法中expr → expr term是左递归直接写成parse_expr里先调用parse_expr会无限递归改成expr → term {(|-) term}后parse_expr先解析一个term然后用while循环反复判断下一个 token 是否/-如果是就继续解析下一个term。这种写法对应了理论课上的“消除左递归”操作把左递归变成了右递归或迭代。测试用例建议覆盖三种情况正常赋值语句、两个运算符连写如a b * c期望报错、括号不匹配。递归下降分析器有个天然的弱点它无法处理需要大范围回溯的语法结构所以如果你的文法有两个产生式都以同一个终结符开头且第二个符号不同必须保证向前看一个 token 足够决策。这就是教材里“LL(1)文法”这个限定条件的实践含义。对这个实验重点检查你的match函数是否在错误发生时推进了pos。很多初版代码在误匹配后不推进位置导致死循环或错误定位。我的做法是匹配失败时也强制前进一个 token避免死循环错误信息保留给用户这样不会无限循环。3.4 实验报告怎么写才能不返工测试用例与异常分支太原理工大学的编译原理实验通常要求交付源码、可运行程序、测试用例和实验报告。大多数人把时间花在源码上报告草草了事结果被退回重写。实际上报告的比重远超你想象——老师要从报告里判断你是真理解还是抄的代码。报告的核心内容应该是三块文法与设计、处理流程、测试结果与分析。文法与设计部分要写出你实现的语言的上下文无关文法并说明它为什么是LL(1)或LR(1)的测试用例部分不能只贴输入输出要解释每个用例覆盖了哪个边界条件比如a12*3;验证运算符优先级a(12)*3;验证括号处理a;验证错误恢复。这个解释过程是老师判断你是否真正会做语法分析实验的关键也是期末笔试“写出分析过程”题的预演。异常分支的处理也值得单列一节。你的词法分析器遇到非法字符如和#是报错还是跳过语法分析器遇到不可匹配的 token 是立即终止还是尽可能恢复到同步点描述清楚这些决策比贴一大堆代码更能体现工程能力。如果老师要求用 Flex/Bison 工具或有更复杂的 C 语言子集实现把上一小节的 Python 代码翻译过去并不难核心的状态转移表、token 流和递归下降函数结构完全不变。4. 编译原理常见翻车现场五个高频踩坑与排查思路4.1 词法分析器“多吞字符”最长匹配与回退没处理好现象输入if(x0)识别出来的第一个 token 居然是if(x而不是if加(。原因状态转移表中标识符状态允许括号字符也跳转或者你的识别循环没有记录最后一次接受状态位置一路走到死路才停止把后续字符都吞进了同一个 token。解决把字符分类做严括号、分号等界符属于独立类别标识符状态遇到界符必须回退。同时加last_accept机制记录读到过的最后一个接受状态的位置和状态编号在遇到不可转移的字符时回退到那个位置。排查时在识别循环里打印每个字符进入的状态和last_accept值一眼能看到吞到哪了。4.2 FIRST/FOLLOW集算错导致LL(1)分析表冲突现象用教材上的经典文法计算FIRST和FOLLOW手算没问题但写程序算出来的分析表里有冲突项。原因多数人的算法漏了“产生式右部多个符号串”的传播。例如A → B CC的FIRST能否加入A的FIRST取决于B是否可推导出空串而FOLLOW的传播则需要多重迭代直到所有集合不再变化为止。用程序实现时循环终止条件是“本轮没有任何集合发生变化”而不是“按产生式序号扫了一遍”。解决把FIRST和FOLLOW的计算独立成两个函数FOLLOW里用while changed: changed False反复迭代每次传播都标记集合是否扩张。测试时用教材第二章和第三章的课后题答案做对照特别是涉及空串产生的ε产生式的文法最容易遗漏。4.3 LR(1)项目集规范族画到一半就乱了现象手工构造 LR(1) 分析表的作业项目集画到第6个就开始丢项目或重复编号导致后面的 ACTION/GOTO 表完全对不上。原因项目集的闭包计算里“展望符号”的传播比大多数人想象的复杂。A → α·Bβ, a中B对应的产生式项目的展望不是a而是FIRST(βa)——这里是整串加展望的FIRST不是单个符号。很多人把FIRST(β)和a混在一起算出错也不奇怪。解决画项目集时用一个表格先列出每个项目集中产生式编号、圆点位置、展望符号、来源项目集和来源符号再画转移边。每个项目集都套这个模板画完立刻核对“每个状态的所有项目展望符号总和是否等于课本对应的项目集”不一致就当场纠正不等画完再查。4.4 符号表作用域处理不对变量声明全串了现象测试程序中有同名变量a外层赋值后进入内层块再给a赋值退出后外层的a值变成了内层的值。原因符号表实现时没有用作用域栈或者变量入表的时机不对——声明和定义的入表时机不同。在某些实现里只要变量名出现在代码中就在当前顶层表中插入条目导致内层变量直接覆盖外层条目。解决改用嵌套符号表或作用域栈。每次遇到{时向栈中压入一张新表遇到}时弹出。查询时从栈顶向下逐表查找找到即返回插入时只插入当前栈顶表。在实验测试里专门设计一段嵌套作用域代码把每个作用域中变量的对应表条目编号打印出来比对结果即可快速定位。4.5 实验报告只贴代码不贴测试答辩被问到怀疑人生现象代码能跑通几个示例报告里没有测试用例设计和结果分析。答辩时老师随便换一个输入程序直接出错答不上来。原因测试用例只是“能跑就行”没有覆盖边界条件也没有思考过哪些输入会导致状态机走入非接受状态、哪些 token 序列会让递归下降分析器报错。解决实验报告固定分为三部分核心代码片段不要全文粘贴、测试用例表输入、期望输出、实际输出、覆盖的知识点、异常输入处理说明。写测试用例时至少覆盖五类正常语句、运算符优先级、括号嵌套、错误输入缺分号、括号不匹配、非法字符、最长匹配测试。答辩时老师问什么直接翻到这个表每条用例都能自圆其说。5. 进阶从“会考试”到“真看懂”——用一个月做一个能编译自己语言的最小编译器如果前面的实验都顺利通过你大概率已经掌握了“翻译”的部分。但要真正理解编译原理还需要一个综合性的收尾项目为一种自定义的极简语言写一个包含词法分析、语法分析、中间代码生成的最小编译器。这个项目不需要产生真实汇编产出三地址码即可。我建议的语言设计如下支持整数变量声明let a 10;、赋值语句a a 1;、while循环while (a 10) { a a 1; }。语法分析部分用递归下降实现中间代码生成采用语法制导翻译——在语法分析的函数中直接调用emit函数输出三地址码。维护一个全局的临时变量计数器遇到每个运算生成新的临时变量编号。下面给出中间代码生成的骨架class CodeGen: def __init__(self): self.temp_count 0 self.label_count 0 self.codes [] def new_temp(self): self.temp_count 1 return ft{self.temp_count} def new_label(self): self.label_count 1 return fL{self.label_count} def emit(self, op, arg1None, arg2None, resultNone): self.codes.append((op, arg1, arg2, result)) def gen_arith(self, op, left_val, right_val): temp self.new_temp() self.emit(op, left_val, right_val, temp) return temp # 生成 while 循环的三地址码 # 形式 # L1: if a 10 goto L2 # goto L3 # L2: a a 1 # goto L1 # L3: def gen_while(self, cond_func, body_func): L1 self.new_label() L2 self.new_label() L3 self.new_label() self.codes.append((LABEL, L1, None, None)) # 假设 cond_func 返回左值关系运算符右值 left, op, right cond_func() self.emit(if, left, right, None) self.codes.append((GOTO_FALSE, L3, None, None)) self.codes.append((LABEL, L2, None, None)) body_func() self.codes.append((GOTO, L1, None, None)) self.codes.append((LABEL, L3, None, None)) def output(self): for c in self.codes: print(c) if __name__ __main__: gen CodeGen() # 模拟生成将 a a 1 嵌入 while (a 10) 循环 gen.gen_while( cond_funclambda: (a, , 10), body_funclambda: gen.emit(, a, 1, a) ) gen.output()这个骨架里有几个关键点。emit函数统一管理所有中间代码的输出格式便于后续统一做优化gen_while中if与GOTO_FALSE的组合是为了避免在生成条件表达式时立即生成跳转而是用标准的两步跳转结构等条件计算结果出现后再填充目标标签——如果你在写真正的编译器这对应“回填”backpatching技术。GOTO_FALSE的意思就是条件不成立时跳转到循环出口这个一目了然的命名方式减少了自己排查错跳转时的认知负担。把这个项目和课内实验串起来你会得到一份完整的综合实践链路词法分析输出的 token 流进入递归下降分析器分析器每归约一个产生式就调用CodeGen生成对应三地址码最后把生成的三地址码用一组手写的测试程序逐个模拟执行。走到这一步你才算是真正“做了一遍编译原理”而不只是“看了一遍PDF”。验证方法很简单给这段代码输入let a 10; while (a 10) { a a 1; }看输出三地址码是否符合预期执行逻辑。如果模拟执行计数得到a10而预期是a最终循环不执行说明条件分支生成有误——这种亲手调试的机会比考前多刷三套卷子有价值得多。回看我的学习过程最庆幸的一件事就是没有停留在“看懂课本”而是每个阶段都强行自己写一遍代码。那个时期的血泪经验后来都成了排查问题时的直觉——看到词法分析异常先怀疑转移表再怀疑回退逻辑看到三地址码生成顺序不对先检查临时变量计数器再检查优先级处理。这种直觉直接决定了你以后看真正的编译器源码比如 TinyCC、LCC时是举重若轻还是寸步难行。希望这篇笔记能帮你少走一些我走过的弯路。本文还有配套的精品资源点击获取