简介这份资源面向高校计算机专业学生尤其是正在学习编译原理课程、需要完成PL/0编译器改造实验的同学。内容围绕教学型编译程序PL/0展开要求对词法分析、语法分析与语义处理模块进行修改扩充包括新增ELSE、FOR、TO、DOWNTO、RETURN等保留字补充、-、、--运算符将不等号#改为并实现条件语句的ELSE子句帮助读者深入理解编译过程的基本原理与实现方法。压缩包共24个文件约644KB包含cpp与h源码、dsp与dsw工程文件、exe可执行程序、doc实验报告以及obj、pdb等编译中间产物覆盖从源码到可运行程序的完整链路。目前已有789人学习下载适合作为课程实验的参考实现与调试对照便于快速定位语法扩充中的关键改动点也可用于撰写实验报告时梳理思路。1. 从一份 PL/0 编译器实验包说起保留字和 ELSE 语句到底卡在哪如果你正在搜“编译原理实验 保留字 PL/0 ELSE语句”大概率是两种情况要么课设要求扩展 PL/0 语法加上 ELSE 分支结果发现词法分析器根本不认这个单词要么老师给的实验包跑不通报错信息指向保留字表但你翻遍代码也没看出哪里写错了。这份实验资源就是围绕 PL/0 编译器的完整实现展开的核心解决两件事第一把保留字从“硬编码字符串比较”改成规范的表驱动识别第二在原有 IF-THEN 语法基础上把 ELSE 子句从词法、语法到代码生成整条链路打通。它适合正在做编译原理课程设计的学生也适合想拿一个小型编译器练手、理解递归下降和符号表管理的开发者。PL/0 本身足够小源码量可控但麻雀虽小五脏俱全——词法、语法、语义、代码生成四个阶段都能摸到。很多人卡在 ELSE 上不是因为逻辑难而是因为 PL/0 原始版本压根没预留这个关键字的位置你得自己动手改词法表和语法产生式。这份资源把改动点拆得比较清楚照着走能少绕几个弯。2. 保留字识别从硬编码到表驱动的改造路径2.1 为什么原始 PL/0 的保留字处理容易翻车PL/0 的经典实现里词法分析器通常用一个getsym过程里面靠一串if-else或者strcmp来逐个比对标识符是不是保留字。这种写法在只有begin、end、if、then、while、do、const、var、procedure、call、read、write、odd这十几个关键字的时候还能忍。但一旦你要加else甚至后面还想加for、repeat、until这种硬编码方式就会变成维护噩梦——每加一个关键字你得在三个地方同步改词法识别、语法产生式、错误处理。更隐蔽的坑是大小写。有些实验包默认全部小写你写ELSE就识别不了有些又要求大小写不敏感但strcmp是敏感的。学生调半天最后发现是Else和else的问题。所以第一步不是急着加else而是先把保留字识别改成表驱动。2.2 用结构体数组管理保留字表常见做法是定义一个struct把保留字的字符串和对应的符号类型绑在一起然后写一个查表函数。这样加新关键字只需要在数组里加一行不用动逻辑。// 保留字表字符串 对应的符号枚举 typedef struct { const char *word; int sym_type; } ReservedWord; // 注意这个表必须按字母序或至少保持稳定顺序方便二分查找 static ReservedWord reserved_words[] { {begin, SYM_BEGIN}, {call, SYM_CALL}, {const, SYM_CONST}, {do, SYM_DO}, {else, SYM_ELSE}, // 新增的 ELSE 关键字 {end, SYM_END}, {if, SYM_IF}, {odd, SYM_ODD}, {procedure,SYM_PROCEDURE}, {read, SYM_READ}, {then, SYM_THEN}, {var, SYM_VAR}, {while, SYM_WHILE}, {write, SYM_WRITE} }; #define RESERVED_COUNT (sizeof(reserved_words) / sizeof(reserved_words[0])) // 查表返回符号类型找不到返回 SYM_IDENTIFIER int lookup_reserved(const char *name) { for (int i 0; i RESERVED_COUNT; i) { if (strcmp(name, reserved_words[i].word) 0) { return reserved_words[i].sym_type; } } return SYM_IDENTIFIER; // 不是保留字就是普通标识符 }这段代码的逻辑很直白词法分析器读完一个单词后先调lookup_reserved如果返回的不是SYM_IDENTIFIER就说明命中了保留字直接把符号类型设成对应的枚举值。参数说明上reserved_words数组的顺序不影响正确性但如果你后面想改成二分查找就得保证按字母序排列。RESERVED_COUNT用宏算出来避免手写长度导致越界。我一般还会在词法阶段加一个大小写归一化的步骤如果实验要求大小写不敏感就在strcmp之前先把单词转成小写。但注意PL/0 的标识符本身可能是大小写敏感的所以归一化只对保留字查表做不要污染标识符本身。2.3 词法分析器里插入查表调用的位置改完表之后得在getsym里找到识别标识符的分支。通常代码长这样读完字母数字串后先默认它是标识符然后拿这个串去查保留字表。如果命中就把sym设成保留字类型否则保持SYM_IDENTIFIER。// 在 getsym 的标识符识别分支中 if (isalpha(ch)) { int len 0; while (isalnum(ch)) { if (len MAX_IDENT_LEN) { id_buffer[len] ch; } ch getch(); } id_buffer[len] \0; ungetch(ch); // 多读了一个字符退回去 // 关键查保留字表 int sym_type lookup_reserved(id_buffer); if (sym_type ! SYM_IDENTIFIER) { sym sym_type; } else { sym SYM_IDENTIFIER; strcpy(id, id_buffer); // 只有普通标识符才存名字 } }这里有个容易忽略的点保留字不需要存到标识符名字缓冲区里。有些实现会把else也存进id结果后面语法分析时拿id去查符号表发现查不到报“未定义标识符”。所以查表命中后直接设sym不要动id。提示改完词法后先单独测保留字识别别急着跑完整程序。写一个只读单词的小驱动把else、ELSE、Else都输进去看sym输出对不对。这一步花五分钟能省后面半小时的调试。3. ELSE 语句的语法扩展递归下降里的分支处理3.1 原始 IF 语句的产生式长什么样PL/0 原始语法里条件语句的产生式大致是statement - if-statement if-statement - if condition then statement注意这里then后面只跟一个statement没有 ELSE 分支。你要加 ELSE就得把产生式改成if-statement - if condition then statement [else statement]方括号表示可选。在递归下降分析器里这意味着解析完then后面的语句后要往前看一个符号如果是else就继续解析 ELSE 分支否则直接返回。3.2 在 statement 解析函数里加 ELSE 分支假设你的语法分析函数叫statement()里面处理if的分支大概是这样// 解析 if 语句 void parse_if() { nextsym(); // 跳过 if parse_condition(); // 解析条件表达式 if (sym ! SYM_THEN) { error(expected then); } nextsym(); // 跳过 then parse_statement(); // 解析 then 分支的语句 // 关键改动检查是否有 else if (sym SYM_ELSE) { nextsym(); // 跳过 else parse_statement(); // 解析 else 分支的语句 } // 没有 else 就什么都不做直接返回 }逻辑说明nextsym()是词法分析器的推进函数每次调用读下一个符号。parse_condition()负责解析条件表达式parse_statement()递归解析语句。加 ELSE 的关键就是在then分支解析完之后判断当前sym是不是SYM_ELSE。如果是消费掉else符号再解析一个语句如果不是说明没有 ELSE 分支直接结束。参数上要注意parse_statement()本身要能处理嵌套的if所以递归调用是必须的。如果你写成循环嵌套 IF 就会出错。另外else的悬挂问题dangling else在 PL/0 里通常约定else匹配最近的if递归下降天然满足这个规则不用额外处理。3.3 代码生成阶段怎么处理 ELSE 跳转语法树建好之后代码生成要把 IF-ELSE 翻译成中间代码或目标代码。PL/0 通常生成类似 P-code 的栈式指令。对于if cond then stmt1 else stmt2逻辑是条件不成立时跳到stmt2的起始位置stmt1执行完后跳过stmt2直接到整个 IF 语句的结束位置。// 伪代码生成 IF-ELSE 的跳转指令 int cond_jump gen_jump_if_false(cond); // 条件为假时跳转回填地址待定 gen_code(stmt1); // 生成 then 分支代码 int end_jump gen_jump(); // 无条件跳转到结束 backpatch(cond_jump, here()); // 条件假跳转的目标 else 开始处 gen_code(stmt2); // 生成 else 分支代码 backpatch(end_jump, here()); // then 分支结束跳转的目标 整个语句结束这里有两个回填点cond_jump要跳到 ELSE 分支的开始end_jump要跳到整个 IF 语句的结束。如果只有 IF 没有 ELSEcond_jump直接回填到结束位置end_jump不需要生成。很多实验包在加 ELSE 时忘了处理end_jump导致 then 分支执行完后又“掉入” else 分支程序行为完全错乱。这个坑我后面还会在排查章节细说。注意回填地址时here()返回当前代码生成位置。如果你用的实验框架有现成的backpatch函数直接调没有的话自己维护一个跳转指令链表最后统一回填。4. 避坑与排查ELSE 扩展中最容易翻车的五个点4.1 现象加了 else 保留字但词法分析报“非法字符”原因通常有两种一是保留字表里加了else但词法分析器的标识符识别循环没把e开头的单词正确读完比如遇到else时只读了el就停了二是大小写问题源码里写的是ELSE但查表用的是小写比较。解决先确认标识符读取循环用的是while (isalnum(ch))能完整读完字母数字串。然后在查表前加一行大小写归一化或者把保留字表里同时放else和ELSE。但更推荐统一转小写再查避免表膨胀。4.2 现象语法分析报“expected then”但源码里明明有 then原因往往是词法阶段把then识别成了普通标识符而不是保留字。检查保留字表里then的拼写和枚举值是否对应。另一个可能是getsym里查表逻辑写反了命中保留字时反而设成了SYM_IDENTIFIER。解决在getsym里加一句调试输出打印每个单词和识别出的sym值。跑一个最小测试用例if a then b else c看then和else的sym是不是预期的枚举值。4.3 现象IF-ELSE 嵌套时else 匹配到了错误的 if这是经典的悬挂 else 问题。比如if a then if b then x else y按照就近匹配原则else应该属于内层if b。但如果语法分析器在解析外层if时提前消费了else就会匹配错。解决递归下降天然就近匹配前提是parse_if里解析完then分支后先检查else再返回。不要在外层提前判断else。如果你用的是其他解析方法比如算符优先那就得显式定义优先级。4.4 现象then 分支执行完又执行了 else 分支这是代码生成阶段最典型的翻车。原因就是前面说的只回填了条件跳转忘了在 then 分支末尾生成无条件跳转。程序跑起来条件为真时执行 then然后“顺流而下”进入 else。解决在生成 then 分支代码后立刻生成一条无条件跳转指令目标地址暂时留空。等 else 分支生成完再回填到结束位置。检查你的代码生成函数确保if有 else 时end_jump一定被创建和回填。4.5 现象符号表里出现了名为 else 的标识符原因是在词法阶段保留字命中后仍然把单词存进了id缓冲区然后语法分析时又把它当成标识符去查符号表。符号表里当然没有else于是报“未定义”。解决查表命中保留字后不要写id缓冲区。只有SYM_IDENTIFIER才需要存名字。检查getsym里strcpy(id, id_buffer)这行是不是放在了if-else的外面。5. 验证与进阶用测试用例把 ELSE 链路跑穿改完词法、语法和代码生成别急着跑大程序。我一般会准备一组最小测试用例从简到繁逐个验证。下面这张表是我常用的检查清单你可以直接抄。测试用例输入源码预期行为检查点保留字识别else词法输出 SYM_ELSE查表命中不存 id简单 IFif a then b条件真执行 b假跳过无 else 时跳转正确简单 IF-ELSEif a then b else c真执行 b假执行 cthen 末尾有无条件跳转嵌套 IFif a then if b then x else yelse 匹配内层 if就近匹配连续 IF-ELSEif a then b else c; if d then e else f两个语句独立跳转地址不串跑完这些基本能覆盖 ELSE 扩展的主要路径。如果还有余力可以试试在 ELSE 分支里再嵌套 WHILE 或 CALL看递归下降的栈深度和符号表作用域有没有问题。进阶一点的做法是给编译器加一个简单的 AST 打印功能。在语法分析阶段每解析完一个 IF-ELSE 节点就打印一下树结构确认then和else分支挂在了正确的父节点下。这个习惯我是在一次嵌套三层 IF 调了一下午之后养成的——从那以后每次改语法产生式我都强制先跑 AST 打印确认树形对了再往下走代码生成。希望这份实验包和上面的排查思路能帮你把 ELSE 这条链路顺利跑通。本文还有配套的精品资源点击获取