资讯详情 Python实现PL0编译器:从词法分析到P-code生成的完整课设指南
📅 2026/10/12 22:37:40
简介面向南京航空航天大学计算机专业学生、也适合其他编译原理初学者的课程设计资源以精简的PL0教学语言为载体使用Python实现从词法分析、语法分析到语义分析与后端代码生成的完整编译器。压缩包共8个文件主体是5个Python脚本分别对应词法分析器、语法分析、语义分析、后端及语义分析测试版本另外附带课程设计报告doc和说明文档md/txt整体仅792KB已有181人学习下载。通过这份资源读者可以运行并调试一个可用的PL0编译器也可以跟着报告梳理“词法分析—语法分析—语义分析—代码生成”的完整链路还能对比测试版与正式版语义分析脚本加深对递归下降分析、抽象语法树和中间代码优化的理解。整体而言这份课设对于完成NUAA作业或自学编译原理都具有不错的参考价值。1. 编译原理课设选PL0Python版编译器四段代码各管一摊编译原理课设做PL0是南京航空航天大学计算机专业的经典实践。PL0是教学用的简化语言语法精简去掉浮点和复杂类型只剩整数、赋值、条件、循环、过程调用这几样却把编译器前端的词法分析、语法分析、语义分析以及后端的目标代码生成串成了一个完整闭环。这份资源用Python写了一个能跑的PL0编译器词法分析器.py、语法分析.py、语义分析.py、后端.py四个模块分开成了独立文件每一段都可以单独改、单独跑。正在做课设、或者想用Python把编译器构造流程快速过一遍的人照着它改最省力。写这份项目的血泪经验是跑通一遍完整链路比对着课本抄三遍第二章都管用。2. 文件构成与设计取舍五个py文件在编译链路里的位置拿到压缩包第一件事是把文件先对一遍。这里不急着跑先看清楚哪个文件对应编译的哪个阶段跑起来出错才知道去哪看。这份资源的结构不算复杂但有两个语义分析文件混在一起不看清单直接动手容易跑错脚本。2.1 文件清单从PL0源码到P-code每一段代码的角色包里的文件对应关系大致如下文件在编译链路里的角色词法分析器.py读入PL0源程序输出token流语法分析.py读token流递归下降识别语句结构语义分析.py建符号表检查变量有无定义、类型是否匹配语义分析_测试版本.py语义分析的调试版带更多打印输出后端.py生成目标代码P-code并输出指令列表article.txt测试用的PL0源程序词法分析从这读入编译原理课设报告.doc课设报告模板README.md简要运行说明先看article.txt它放的是测试用的PL0源程序。常见课设里放的是一段求最大公约数或阶乘的代码结构大致是这个味道VAR a, b, c; BEGIN a : 12; b : 8; WHILE a # b DO IF a b THEN a : a - b ELSE b : b - a; WRITE a END.这段程序里有两个容易在第一次跑时吓住人的点。一个是#号它代表“不等于”PL0里刻意不用!这种现代写法另一个是语句结束的分号规则WHILE后的IF语句整行直接就是一条语句不需要额外包BEGIN END。看清这两个符号词法分析那一关就好过了。语法分析.py读的不是article.txt原文而是读词法分析器产出的token流。词法分析器.py输出一个[(符号, 值), ...]列表语法分析器再基于这个列表做递归下降。两个文件之间不直接共享源码字符串这是PL0课设最常见的代码组织方式词法分析完整跑完才轮到语法分析。这种分段式的结构对调试非常友好词法阶段出了问题不用碰语法分析的任何逻辑。2.2 三个设计取舍Python单遍、P-code输出、双语义分析文件这个项目有几个设计决策带过很多份课设我认为值得单独说。理解了取舍后续改代码才知道往哪个方向改。第一个决策是用Python做单遍编译。词法分析产出token流之后语法分析边下降边调后端生成指令符号表也是边扫边建的。这是教学编译器的最常见方案适合控制代码量因为课程设计核心是跑通链路不是做优化。如果换成两遍编译中间还要多维护一棵抽象语法树代码量直接翻一倍对于课程设计来说性价比不高。Python在这里的优势不是性能而是列表、字典、元组这些内置数据结构和语法分析器的思路天然对齐写起来几乎没有心智负担。第二个决策是后端直接生成P-code。P-code是栈式虚拟机指令指令集很小每行形如LIT 0 12、STO 0 1这种三列结构人工可读性极强。对比一下如果生成x86汇编光是寄存器分配和栈帧结构就得写上千行对于一个周数有限的课设来说这个复杂度是灾难级的。P-code的好处是能打印成文本文件报告里直接贴出来就是一张很好的成果图答辩时指着说“这是中间代码”就够用了。第三个决策也是最容易让人疑惑的为什么有两个语义分析文件。语义分析_测试版本.py和语义分析.py前者通常是带了符号表dump、token流打印的调试版后者是干净版。我的建议是跑的时候以语义分析.py为准测试版本只用来对照看中间状态。很多同学拿到包先打开测试版本被五六十行打印输出搅晕其实那份是给人看过程用的不是最终跑结果用的。区分两个文件的方法很简单看有没有print调用有打印的是调试版。2.3 PL0文法里最关键的几条产生式PL0的文法一周内能背下来核心就这么几条分程序 :: 常量说明 变量说明 过程说明 语句 语句 :: 标识符 : 表达式 | BEGIN 语句 {; 语句} END | IF 条件 THEN 语句 | WHILE 条件 DO 语句 | CALL 标识符 | READ 标识符 | WRITE 表达式 表达式 :: [|-] 项 {(|-) 项} 项 :: 因子 {(*|/) 因子} 因子 :: 标识符 | 数字 | ( 表达式 )特别注意“语句 :: IF 条件 THEN 语句”这一条它解释了为什么IF后面可以再跟IF而不需要END只要THEN后面是一个合法语句就行。嵌套是靠“语句”递归展开的不是靠括号。理解这一点递归下降写起来会顺手很多因为每个非终结符对应一个函数嵌套天然就是函数调函数的形态。还有一种常见误解是把BEGIN END和IF THEN配对当成一回事。BEGIN END是复合语句包住一串以分号分隔的语句IF THEN的语句体如果只有一条可以不加BEGIN END。两者混用是初写PL0时最容易产生语法错误的地方。我见过很多同学在IF的THEN后面习惯性写BEGIN结果BEGIN和END的数量对不上语法分析一路报错这就是文法级别没理解透的表现。3. 词法与语法分析token流和递归下降的落地写法编译课的课本把词法分析讲得很理论状态转换图、有限自动机那一套但落到课设上要处理的就三类素材保留字、标识符和数字、运算符和界符。把这三类识别清楚词法分析就过关了。语法分析这边递归下降的思路是所有非终结符都用函数实现这个模式对PL0这种小文法足够且直观。3.1 词法分析保留字识别放在标识符之后词法分析器.py的核心逻辑大致是这个样子# 词法分析器.py 核心片段 # 保留字表PL0全部保留字先组词后查表 KEYWORDS { const: constsym, var: varsym, procedure: procsym, begin: beginsym, end: endsym, if: ifsym, then: thensym, while: whilesym, do: dosym, call: callsym, read: readsym, write: writesym, odd: oddsym } # 双字符运算符PL0只有 和 TWO_CHAR_OPS {, } def next_symbol(src, i): 从 src[i] 开始读一个 token返回 (符号类型, 语义值, 新下标) n len(src) while i n and src[i] in \t\n\r: i 1 if i n: return (period, None, i) ch src[i] if ch.isalpha(): start i while i n and (src[i].isalpha() or src[i].isdigit()): i 1 word src[start:i] if word in KEYWORDS: return (KEYWORDS[word], None, i) return (ident, word, i) if ch.isdigit(): start i while i n and src[i].isdigit(): i 1 return (number, int(src[start:i]), i) if i 1 n and src[i:i2] in TWO_CHAR_OPS: op src[i:i2] return ({: leq, : geq}[op], None, i 2) # 单字符符号 single {: plus, -: minus, *: times, /: slash, : eql, #: neq, : lss, : gtr, (: lparen, ): rparen, ,: comma, ;: semicolon, .: period} if ch in single: return (single[ch], None, i 1) raise ValueError(funexpected char: {ch!r} at position {i})这个函数的关键在于处理保留字和标识符的顺序先组词再查表。如果写反了遇到“ifx”这个用户自定义标识符就会因为前两个字符是if而被误判成保留字。PL0语言虽然小但课设报告里对这种边界情况一向查得严。保留字表用了大写的symbol名是为了和语法分析里的判断保持一致语法分析直接用字符串比较不用维护第二套映射关系。参数方面src是读入的整个源码字符串i是当前扫描下标。返回值故意设计成三元组而不是把下标放到对象里这样词法分析器是无状态的下一段token从哪开始完全由返回值决定。数字识别必须一次吃干净连续的数字字符再把整段转成int否则“12”会被拆成两个token。运算符部分先尝试双字符匹配不到再走单字符分支这个顺序不能反否则“”会被拆成“”和“”。实际跑的时候词法分析器.py读article.txt只需要三行open读文件、while循环调next_symbol、把token收集到列表里。输出到控制台时每一行形如(number, 12)、(ident, a)、becomes。看到becomes出现在正确位置词法分析就过关了。调试时如果发现token流里混进了汉字或者奇怪的符号问题基本不在词法逻辑而在文件编码这个在第5章会专门讲。3.2 递归下降表达式为什么用循环而不是递归PL0的表达式产生式是“表达式 :: 项 {(|-) 项}”花括号表示0到多个。最直接、也最坑的实现是上来先调expression()自身这就成了左递归在Python里会直接触发RecursionError。正确的写法是先处理一个项再用while循环吃掉剩余的同优先级运算符。# 语法分析.py 核心片段 class Parser: def __init__(self, tokens): self.tokens tokens # 词法分析产出的 (sym, val) 列表 self.pos 0 self.sym, self.val tokens[0] def advance(self): 吃掉当前 token推进到下一个 self.pos 1 if self.pos len(self.tokens): self.sym, self.val self.tokens[self.pos] else: self.sym, self.val period, None def factor(self): 因子标识符 | 数字 | ( 表达式 ) if self.sym ident: name self.val self.advance() self.semantic.emit_ident(name) elif self.sym number: num self.val self.advance() self.backend.gen(LIT, 0, num) elif self.sym lparen: self.advance() self.expression() if self.sym ! rparen: raise SyntaxError(missing )) self.advance() else: raise SyntaxError(funexpected token: {self.sym}) def term(self): 项因子 {(*|/) 因子} self.factor() while self.sym in (times, slash): op self.sym self.advance() self.factor() self.backend.gen(OPR, 0, 3 if op times else 4) def expression(self): 表达式[|-] 项 {(|-) 项} if self.sym in (plus, minus): self.backend.gen(OPR, 0, 5) # 取负 self.advance() self.term() while self.sym in (plus, minus): op self.sym self.advance() self.term() self.backend.gen(OPR, 0, 1 if op plus else 2)每递归一层就至少吃掉一个token这是递归下降不会栈溢出的底线。factor里遇到数字就立即生成LIT指令遇到标识符就交给语义分析登记这一行的顺序决定了符号表能拿到正确的名字。term里的while循环处理乘除法每轮循环都会调用factor而factor内部又会调用expression处理括号所以括号嵌套的层级无论多深最终都会因为token被消耗完而自然结束。这套代码有几个参数值得注意。self.sym是当前token的类型self.val是对应的值self.pos是token流里的下标。递归下降里非常忌讳不用advance()而直接改pos那会让parsing逻辑散落到各处查错时根本不知道当前状态在哪一层。后端指令用(OPR, 0, 1)这种三元组传参第0位是指令名第1位是层差第2位是子功能号。不同的运算符映射到不同的子功能号1是加、2是减、3是乘、4是除这个表在后面第4章会完整列出来。3.3 把token流接进语法分析一个简单的驱动循环语法分析的主体是递归下降但顶层还需要一个驱动循环。PL0的简单课设通常只编译一个分程序读到period句点就结束# 语法分析.py 顶层驱动 with open(article.txt, encodingutf-8) as f: src f.read() tokens [] i 0 while True: sym, val, i next_symbol(src, i) tokens.append((sym, val)) if sym period: break parser Parser(tokens) parser.block() # 分程序 if parser.sym ! period: raise SyntaxError(fexpected period, got {parser.sym})这个驱动循环把词法分析和语法分析拼接起来的方式是很多同学第一次接触时最困惑的。词法分析的两层循环要分清外层循环控制整个文件读完内层是next_symbol内部跳过空白字符的逻辑。如果内层循环的停止条件写错会出现跳过不了换行的诡异现象——第一行能解析第二行开始全部变成乱码。语法分析里有一个很容易遗漏的点读文件时open的编码参数。在Windows上只写open(article.txt)容易沿系统默认编码打开而文件实际是UTF-8跑出来的现象不是报错而是第一个汉字乱码后整个语法树错位。我一般会强制写encodingutf-8养成习惯以后能少踩很多坑。顶层驱动和递归下降函数是两层逻辑驱动管文件级循环递归下降管语句级嵌套两者职责分开后面加trace调试时改动只在驱动层不会波及文法函数。4. 语义分析与后端符号表、P-code与双语义文件之谜语法分析只认结构不认含义。a : b结构上没问题但b到底有没有声明、是常量还是变量这要语义分析来管。后端则负责把语法分析过程中顺路生成的动作翻译成P-code指令。这一步做完编译器从源码到中间代码的完整链路就通了。4.1 符号表三个字段加一张按层叠放的表PL0的符号表不复杂每个符号记录三个字段就够了# 语义分析.py 核心片段 class Symbol: def __init__(self, name, kind, level, addr): self.name name # 标识符原名 self.kind kind # const | var | proc self.level level # 所在层主程序为0 self.addr addr # 常量值 / 变量偏移 / 过程入口 class SymbolTable: def __init__(self): self.tables [] self.current_level 0 def enter_block(self): 进入新的分程序新建一层符号表 self.tables.append([]) self.current_level 1 def leave_block(self): 退出分程序整层弹掉 self.tables.pop() self.current_level - 1 def enter(self, sym): self.tables[self.current_level].append(sym) def lookup(self, name): 由内向外找内层同名符号优先 for level in range(self.current_level, -1, -1): for s in reversed(self.tables[level]): if s.name name: return s return Nonelookup的查找方向是这段代码的核心从当前层一路查回全局。PL0允许内层过程和外层变量同名查表必须保证先命中内层。如果一个课设版本的符号表是单一字典不按层压栈嵌套过程一多就会读出错误的值。reversed是为了让同层内后声明的先被找到这符合多数PL0程序的视觉习惯但实际影响不大重点是层间的顺序。enter_block和leave_block在语法分析的block()入口和出口各调用一次成对出现。这一步很容易漏漏了之后的现象很迷惑内层过程结束后它声明的变量仍然留在符号表里外层后续代码可能错误地引用到。符号表在语义分析里的角色是“登记处”语法分析遇到标识符就调emit_ident登记遇到赋值就查表验证左侧是变量遇到过程调用就查表确认是过程。这些查证做完语义错误才能被挡在编译期而不是运行期。4.2 后端P-code指令集小到能背下来后端.py的功能是提供gen()方法把中间代码逐条写进列表。PL0教程的P-code指令集总共就十几条指令参数含义LIT常量值把常量压栈LOD层差、变量地址读变量值压栈STO层差、变量地址弹栈写入变量CAL层差、过程入口调用过程INT栈帧长度为局部变量分配空间JMP目标指令号无条件跳转JPC目标指令号条件跳转条件为假时跳OPR子功能号算术、比较、读写、返回OPR的子功能号需要单独记0返回、1加、2减、3乘、4除、5取负、6等于、7不等、8小于、9大于等于、10大于、11小于等于、12输出、13读入。后端.py的gen方法实现很简单# 后端.py 核心片段 class Backend: def __init__(self): self.code [] def gen(self, op, level, arg): 生成一条指令。op 是指令名level 是层差arg 是参量 self.code.append((op, level, arg)) def dump(self): 打印指令列表报告里直接复制这段输出 for idx, (op, l, a) in enumerate(self.code): print(f{idx:4d} {op:3s} {l} {a})gen方法在语法分析阶段被反复调用但此时不需要知道指令序号——跳转指令的目标地址靠回填。常见做法是JMP/JPC先填一个占位的-1等目标位置确定后遍历代码列表把-1换回真实行号。后端.py里通常还会留一个pass_two()做这个回填这一步不能省否则跑出来的代码一到WHILE就乱跳。层差是一个很容易搞混的参数。它记录的是“当前层”到“变量所在层”的差值而不是层号的绝对值。主程序变量级别是0某个过程里的变量级别是1那语法分析在过程内部引用主程序变量时层差就是1。这个参数在报告里经常被追问一定要分清。P-code虽然不直接生成机器码但它的栈式结构和真实的JVM字节码、Python字节码是同一个思路理解了P-code后面学任何虚拟机都有参照物。4.3 测试版和正式版语义分析用打印量区分定位压缩包里的语义分析_测试版本.py我在前面的文件清单里说它是带调试输出的版本。两版代码逻辑基本一致差异集中在三点测试版会打印每一条enter/lookup调用、会dump符号表全貌、会在leave_block时打印被弹出的符号。正式版则只保留符号表的静默维护。调试时先用测试版跑一次确认符号表进出正常交代码时换成正式版把打印注释掉。如果你拿到的是别人的包不确定哪个文件是哪个版本打开文件看有没有print调用一眼就能区分。测试版的价值在于它把符号表这个黑匣子里的状态显式吐出来遇到“变量未定义”类的报错对照打印结果就能看出是没登记成功还是查表顺序错了。正式版的价值在于代码干净、逻辑集中适合作为最终提交的版本。这里有一个实操建议把article.txt换成自己写的PL0测试程序时先跑测试版语义分析确认新程序里的变量都被符号表正确登记再跑完整编译。这样可以把语义问题和词法语法问题分开排查。如果新程序里用到了过程嵌套尤其要看leave_block时的打印确认每次弹出都只弹当前层。5. 复现与避坑跑通PL0时的五个常见问题与修法复现顺序不复杂先跑词法分析器读article.txt再跑语法分析中途会调语义分析和后端最后看后端输出的P-code。问题是正因为它流程短每个环节的错会被后面环节放大词法漏一个字符语法就开始连环报错。以下五条是我在这份资源上复现时遇到的高频问题全部按现象到原因再到解决来写。这五个坑里前三个是代码逻辑问题后两个是环境和预处理问题环境问题往往比逻辑问题更隐蔽。5.1 RecursionError左递归把Python调用栈打穿现象语法分析一跑就报RecursionError: maximum recursion depth exceeded或者控制台完全没输出直接崩溃好像整个分析器停在了原地。原因expression()的实现走了左递归——函数在最开头又调用了自己。PL0的表达式产生式是“表达式 :: 项 {(|-) 项}”花括号表示重复但很多手写实现会图省事把expression()写成先调用expression()再处理项。这种写法在理论上等价于文法左递归Python解释器的栈一压就爆。文法里没有任何一个token被消耗递归就不会结束。解决把表达式改成先term()再用while循环吃掉后续的加减号配合advance()保证每轮至少前进一步。凡是产生式里出现花括号即{...}一律用循环实现不递归。factor里处理括号时倒是可以递归调expression那是因为括号本身消耗了一个token递归深度受源码嵌套层数限制。验证方法改写后跑一遍观察第6章加的trace输出如果expression进入后sym的值从ident变成了plus说明结构正确。如果还爆栈检查是不是两个非终结符互相调用时都没有先advance。5.2 数字被拆成两个token现象源码里写a : 12语法分析收到的却是两个number1和2赋值语句直接被拆烂。原因数字循环的停止条件写在了下标更新之前或者用了isdigit()和isalpha()混合判断导致第一个数字字符处理完后循环条件已经失效。另一个常见错误是把isalpha判断放在isdigit之前遇到“12a”这种非法程序时会把“12”解析成数字后再把“a”当标识符报错位置离真实问题隔着好几行。解决数字分支统一用while i n and src[i].isdigit()先判断下标越界再取字符循环结束后用int(src[start:i])一次性转成整数。注意把条件顺序固定成“先i n再取字符”这个顺序写反最后一个字符会直接越界。识别完数字后要立即返回不要在数字分支后面继续走运算符判断。验证方法直接打印词法分析输出确认(number, 12)是单个token而不是两个。顺带检查“12a”这种非法程序是否能报出期望的错如果没报说明词法分析把标识符和数字的边界放得太宽了。5.3 嵌套过程结束后符号表里还留着内层变量现象外层过程里引用了内层过程的变量名编译竟然通过了运行结果却乱七八糟且报错位置总在跳转指令附近。原因符号表是单一列表没有按分程序压栈弹栈。内层过程退出时leave_block没有把整层符号弹掉内层声明就污染了外层作用域。这种问题的隐蔽性在于它不会报“变量未定义”反而会报出一堆和变量无关的跳转错误容易把人引到后端调试上浪费时间。解决检查block()入口有没有enter_block、出口有没有leave_block两个调用必须成对出现。想快速定位就用测试版本语义分析退出内层后dump一次符号表看残留符号一眼可见。leave_block实现时不要清空整个tables列表只pop当前层否则内层声明会全部丢失。验证方法在leave_block后打印符号表里当前层的符号数量如果内层退出后符号数没减少说明弹栈逻辑缺失。用两个同名变量分别在外层和内层声明跑一遍测试版本看lookup最终命中哪一层。5.4 Windows下运行报错或输出乱码现象运行词法分析器.py时报UnicodeDecodeError或者终端里出现“锘”之类花字看起来代码本身没问题但就是跑不通。这个坑最迷惑人的地方在于不是每台机器都会出和编辑器的保存编码有关。原因article.txt是UTF-8编码而Windows默认用GBK打开文本更常见的场景是py文件里写了中文注释控制台按GBK解释源码时把代码里的中文字符串读成乱码。Python 3虽然在源码层面默认UTF-8但open()函数如果不指定编码在Windows上会用locale.getpreferredencoding()也就是GBK。解决所有open操作写死encodingutf-8源码文件头部加# -- coding: utf-8 --如果终端实在显示乱码把代码文件的编码统一另存为UTF-8。检查一下你的编辑器右下角如果显示的是GBK或ANSI立刻另存为UTF-8再跑。验证方法换个目录重跑一次同一份文件在不同环境下结果一致才算处理干净。如果在命令行下还是有乱码执行chcp 65001切到UTF-8代码页再试。5.5 article.txt里混进中文注释或“”占位符现象词法分析在某个中文字符处直接raise ValueError或者把“”识别成非法字符整个分析中断在源码的中间位置。原因PL0标准语法里没有“”这个符号而有些课设报告里用“”占位中文注释混在源码中时Python的isalpha()会把中文字符判定为字母词法分析会把一整段中文误认成标识符。这是词法分析里最典型的“文本预处理没做”问题。由于词法分析器逐字符扫描它不会自己跳过注释注释处理必须由外层先做完。解决正式编译前先剥注释。最稳妥的方式是写一个三行的预处理函数把非ASCII字符和注释块直接滤掉再喂给next_symbol。不要指望词法分析器能天然跳过注释——这份课设也没有在词法层处理注释。预处理函数要注意不要误删字符串里的字符不过PL0没有字符串类型所以可以直接过滤非ASCII。验证方法预处理后跑词法分析输出里不出现ident值为中文的token。如果还有残留问题比如“”没被滤干净把预处理函数的过滤条件从“非ASCII”扩展成“不在PL0合法字符集”一劳永逸。6. 进阶技巧给语法分析加trace跟踪半小时定位死循环课设跑到后期很多人会卡在语法分析上明明感觉语句结构没错分析器却停不住或跳转错误。这时候不要一行行读代码直接给语法分析套一层跟踪。我习惯做法是给Parser加一个可选的trace开关在advance()里打印每一步的token状态# 语法分析.py 中加一段可选的 trace class Parser: def __init__(self, tokens, traceFalse): self.tokens tokens self.pos 0 self.sym, self.val tokens[0] self.trace trace def advance(self): 带 trace 的 token 推进调试时能看到每次消费的 token if self.trace: print(f[{self.pos:4d}] consume {self.sym} val{self.val}) self.pos 1 if self.pos len(self.tokens): self.sym, self.val self.tokens[self.pos] else: self.sym, self.val period, None再配一个进栈式跟踪在每个非终结符函数上打点def trace_rule(name): 装饰器打印进入和退出某个文法规则的时刻 def deco(fn): def wrapper(self, *args): print(f enter {name}, sym{self.sym}) r fn(self, *args) print(f exit {name}, sym{self.sym}) return r return wrapper return deco用法是在expression、term、factor这些函数定义前加trace_rule(expression)。跑一次就能看到每次进入表达式时当前token是什么。如果发现某个符号被反复进入却不消耗就是死递归如果进入了一个函数后sym永远不变就是advance没有生效。这套调试方法解决了我当年最头疼的一个问题WHILE语句嵌套里漏写了一个advance()整个分析器卡在同一个token上转圈。开了trace之后打印里清楚显示每次进入while子句时sym都停在同一个标识符上问题位置一眼就能圈定。从那以后我每次调试语法分析都先开trace再跑打出来的堆栈比猜代码快得多也稳定得多。如果你也卡在这个课设的某个奇怪死循环里不妨先加这一行打印再往下查。希望帮到你。本文还有配套的精品资源点击获取